telegram_bot.py 128 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978
  1. #!/usr/bin/env python3
  2. """
  3. Telegram Bot for Hyperliquid Trading
  4. This module provides a Telegram interface for manual Hyperliquid trading
  5. with comprehensive statistics tracking and phone-friendly controls.
  6. """
  7. import logging
  8. import asyncio
  9. import re
  10. from datetime import datetime, timedelta
  11. from typing import Optional, Dict, Any
  12. from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
  13. from telegram.ext import Application, CommandHandler, CallbackQueryHandler, ContextTypes, MessageHandler, filters
  14. from hyperliquid_client import HyperliquidClient
  15. from trading_stats import TradingStats
  16. from config import Config
  17. from alarm_manager import AlarmManager
  18. from logging_config import setup_logging, cleanup_logs, format_log_stats
  19. # Set up logging using the new configuration system
  20. logger = setup_logging().getChild(__name__)
  21. class TelegramTradingBot:
  22. """Telegram bot for manual trading with comprehensive statistics."""
  23. def __init__(self):
  24. """Initialize the Telegram trading bot."""
  25. self.client = HyperliquidClient(use_testnet=Config.HYPERLIQUID_TESTNET)
  26. self.stats = TradingStats()
  27. self.alarm_manager = AlarmManager()
  28. self.authorized_chat_id = Config.TELEGRAM_CHAT_ID
  29. self.application = None
  30. # Order monitoring
  31. self.monitoring_active = False
  32. self.last_known_orders = set() # Track order IDs we've seen
  33. self.last_known_positions = {} # Track position sizes for P&L calculation
  34. # External trade monitoring
  35. self.last_processed_trade_time = None # Track last processed external trade
  36. # Initialize stats with current balance
  37. self._initialize_stats()
  38. def _initialize_stats(self):
  39. """Initialize stats with current balance."""
  40. try:
  41. balance = self.client.get_balance()
  42. if balance and balance.get('total'):
  43. # Get USDC balance as the main balance
  44. usdc_balance = float(balance['total'].get('USDC', 0))
  45. self.stats.set_initial_balance(usdc_balance)
  46. except Exception as e:
  47. logger.error(f"Could not initialize stats: {e}")
  48. def is_authorized(self, chat_id: str) -> bool:
  49. """Check if the chat ID is authorized to use the bot."""
  50. return str(chat_id) == str(self.authorized_chat_id)
  51. async def send_message(self, text: str, parse_mode: str = 'HTML') -> None:
  52. """Send a message to the authorized chat."""
  53. if self.application and self.authorized_chat_id:
  54. try:
  55. await self.application.bot.send_message(
  56. chat_id=self.authorized_chat_id,
  57. text=text,
  58. parse_mode=parse_mode
  59. )
  60. except Exception as e:
  61. logger.error(f"Failed to send message: {e}")
  62. async def start_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  63. """Handle the /start command."""
  64. if not self.is_authorized(update.effective_chat.id):
  65. await update.message.reply_text("โŒ Unauthorized access.")
  66. return
  67. welcome_text = """
  68. ๐Ÿค– <b>Hyperliquid Manual Trading Bot</b>
  69. Welcome to your personal trading assistant! Control your Hyperliquid account directly from your phone.
  70. <b>๐Ÿ“ฑ Quick Actions:</b>
  71. Tap the buttons below for instant access to key functions.
  72. <b>๐Ÿ’ผ Account Commands:</b>
  73. /balance - Account balance
  74. /positions - Open positions
  75. /orders - Open orders
  76. /stats - Trading statistics
  77. <b>๐Ÿ“Š Market Commands:</b>
  78. /market - Market data (default token)
  79. /market SOL - Market data for SOL
  80. /price - Current price (default token)
  81. /price BTC - Price for BTC
  82. <b>๐Ÿš€ Perps Trading:</b>
  83. โ€ข /long BTC 100 - Long BTC with $100 USDC (Market Order)
  84. โ€ข /long BTC 100 45000 - Long BTC with $100 USDC at $45,000 (Limit Order)
  85. โ€ข /short ETH 50 - Short ETH with $50 USDC (Market Order)
  86. โ€ข /short ETH 50 3500 - Short ETH with $50 USDC at $3,500 (Limit Order)
  87. โ€ข /exit BTC - Close BTC position with Market Order
  88. <b>๐Ÿ›ก๏ธ Risk Management:</b>
  89. โ€ข /sl BTC 44000 - Set stop loss for BTC at $44,000
  90. โ€ข /tp BTC 50000 - Set take profit for BTC at $50,000
  91. <b>๐Ÿšจ Automatic Stop Loss:</b>
  92. โ€ข Enabled: {risk_enabled}
  93. โ€ข Stop Loss: {stop_loss}% (automatic execution)
  94. โ€ข Monitoring: Every {heartbeat} seconds
  95. <b>๐Ÿ“‹ Order Management:</b>
  96. โ€ข /orders - Show all open orders
  97. โ€ข /orders BTC - Show open orders for BTC only
  98. โ€ข /coo BTC - Cancel all open orders for BTC
  99. <b>๐Ÿ“ˆ Statistics & Analytics:</b>
  100. โ€ข /stats - Complete trading statistics
  101. โ€ข /performance - Token performance ranking & detailed stats
  102. โ€ข /daily - Daily performance (last 10 days)
  103. โ€ข /weekly - Weekly performance (last 10 weeks)
  104. โ€ข /monthly - Monthly performance (last 10 months)
  105. โ€ข /risk - Sharpe ratio, drawdown, VaR
  106. โ€ข /trades - Recent trade history
  107. <b>๐Ÿ”” Price Alerts:</b>
  108. โ€ข /alarm - List all alarms
  109. โ€ข /alarm BTC 50000 - Set alarm for BTC at $50,000
  110. โ€ข /alarm BTC - Show BTC alarms only
  111. โ€ข /alarm 3 - Remove alarm ID 3
  112. <b>๐Ÿ”„ Automatic Monitoring:</b>
  113. โ€ข Real-time order fill alerts
  114. โ€ข Position opened/closed notifications
  115. โ€ข P&L calculations on trade closure
  116. โ€ข Price alarm triggers
  117. โ€ข External trade detection & sync
  118. โ€ข Auto stats synchronization
  119. โ€ข {heartbeat}-second monitoring interval
  120. <b>๐Ÿ“Š Universal Trade Tracking:</b>
  121. โ€ข Bot trades: Full logging & notifications
  122. โ€ข Platform trades: Auto-detected & synced
  123. โ€ข Mobile app trades: Monitored & recorded
  124. โ€ข API trades: Tracked & included in stats
  125. Type /help for detailed command information.
  126. <b>๐Ÿ”„ Order Monitoring:</b>
  127. โ€ข /monitoring - View monitoring status
  128. โ€ข /logs - View log file statistics and cleanup
  129. <b>โš™๏ธ Configuration:</b>
  130. โ€ข Symbol: {symbol}
  131. โ€ข Default Token: {symbol}
  132. โ€ข Network: {network}
  133. <b>๐Ÿ›ก๏ธ Safety Features:</b>
  134. โ€ข All trades logged automatically
  135. โ€ข Comprehensive performance tracking
  136. โ€ข Real-time balance monitoring
  137. โ€ข Risk metrics calculation
  138. <b>๐Ÿ“ฑ Mobile Optimized:</b>
  139. โ€ข Quick action buttons
  140. โ€ข Instant notifications
  141. โ€ข Clean, readable layout
  142. โ€ข One-tap commands
  143. For support, contact your bot administrator.
  144. """.format(
  145. symbol=Config.DEFAULT_TRADING_TOKEN,
  146. network="Testnet" if Config.HYPERLIQUID_TESTNET else "Mainnet",
  147. risk_enabled=Config.RISK_MANAGEMENT_ENABLED,
  148. stop_loss=Config.STOP_LOSS_PERCENTAGE,
  149. heartbeat=Config.BOT_HEARTBEAT_SECONDS
  150. )
  151. keyboard = [
  152. [
  153. InlineKeyboardButton("๐Ÿ’ฐ Balance", callback_data="balance"),
  154. InlineKeyboardButton("๐Ÿ“Š Stats", callback_data="stats")
  155. ],
  156. [
  157. InlineKeyboardButton("๐Ÿ“ˆ Positions", callback_data="positions"),
  158. InlineKeyboardButton("๐Ÿ“‹ Orders", callback_data="orders")
  159. ],
  160. [
  161. InlineKeyboardButton("๐Ÿ’ต Price", callback_data="price"),
  162. InlineKeyboardButton("๐Ÿ“Š Market", callback_data="market")
  163. ],
  164. [
  165. InlineKeyboardButton("๐Ÿ”„ Recent Trades", callback_data="trades"),
  166. InlineKeyboardButton("โš™๏ธ Help", callback_data="help")
  167. ]
  168. ]
  169. reply_markup = InlineKeyboardMarkup(keyboard)
  170. await update.message.reply_text(welcome_text, parse_mode='HTML', reply_markup=reply_markup)
  171. async def help_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  172. """Handle the /help command."""
  173. if not self.is_authorized(update.effective_chat.id):
  174. await update.message.reply_text("โŒ Unauthorized access.")
  175. return
  176. help_text = """
  177. ๐Ÿ”ง <b>Hyperliquid Trading Bot - Complete Guide</b>
  178. <b>๐Ÿ’ผ Account Management:</b>
  179. โ€ข /balance - Show account balance
  180. โ€ข /positions - Show open positions
  181. โ€ข /orders - Show open orders
  182. <b>๐Ÿ“Š Market Data:</b>
  183. โ€ข /market - Detailed market data (default token)
  184. โ€ข /market BTC - Market data for specific token
  185. โ€ข /price - Quick price check (default token)
  186. โ€ข /price SOL - Price for specific token
  187. <b>๐Ÿš€ Perps Trading:</b>
  188. โ€ข /long BTC 100 - Long BTC with $100 USDC (Market Order)
  189. โ€ข /long BTC 100 45000 - Long BTC with $100 USDC at $45,000 (Limit Order)
  190. โ€ข /short ETH 50 - Short ETH with $50 USDC (Market Order)
  191. โ€ข /short ETH 50 3500 - Short ETH with $50 USDC at $3,500 (Limit Order)
  192. โ€ข /exit BTC - Close BTC position with Market Order
  193. <b>๐Ÿ›ก๏ธ Risk Management:</b>
  194. โ€ข /sl BTC 44000 - Set stop loss for BTC at $44,000
  195. โ€ข /tp BTC 50000 - Set take profit for BTC at $50,000
  196. <b>๐Ÿšจ Automatic Stop Loss:</b>
  197. โ€ข Enabled: {risk_enabled}
  198. โ€ข Stop Loss: {stop_loss}% (automatic execution)
  199. โ€ข Monitoring: Every {heartbeat} seconds
  200. <b>๐Ÿ“‹ Order Management:</b>
  201. โ€ข /orders - Show all open orders
  202. โ€ข /orders BTC - Show open orders for BTC only
  203. โ€ข /coo BTC - Cancel all open orders for BTC
  204. <b>๐Ÿ“ˆ Statistics & Analytics:</b>
  205. โ€ข /stats - Complete trading statistics
  206. โ€ข /performance - Win rate, profit factor, etc.
  207. โ€ข /risk - Sharpe ratio, drawdown, VaR
  208. โ€ข /trades - Recent trade history
  209. <b>๐Ÿ”” Price Alerts:</b>
  210. โ€ข /alarm - List all active alarms
  211. โ€ข /alarm BTC 50000 - Set alarm for BTC at $50,000
  212. โ€ข /alarm BTC - Show all BTC alarms
  213. โ€ข /alarm 3 - Remove alarm ID 3
  214. <b>๐Ÿ”„ Order Monitoring:</b>
  215. โ€ข /monitoring - View monitoring status
  216. โ€ข /logs - View log file statistics and cleanup
  217. <b>โš™๏ธ Configuration:</b>
  218. โ€ข Symbol: {symbol}
  219. โ€ข Default Token: {symbol}
  220. โ€ข Network: {network}
  221. <b>๐Ÿ›ก๏ธ Safety Features:</b>
  222. โ€ข All trades logged automatically
  223. โ€ข Comprehensive performance tracking
  224. โ€ข Real-time balance monitoring
  225. โ€ข Risk metrics calculation
  226. <b>๐Ÿ“ฑ Mobile Optimized:</b>
  227. โ€ข Quick action buttons
  228. โ€ข Instant notifications
  229. โ€ข Clean, readable layout
  230. โ€ข One-tap commands
  231. For support, contact your bot administrator.
  232. """.format(
  233. symbol=Config.DEFAULT_TRADING_TOKEN,
  234. network="Testnet" if Config.HYPERLIQUID_TESTNET else "Mainnet",
  235. risk_enabled=Config.RISK_MANAGEMENT_ENABLED,
  236. stop_loss=Config.STOP_LOSS_PERCENTAGE,
  237. heartbeat=Config.BOT_HEARTBEAT_SECONDS
  238. )
  239. await update.message.reply_text(help_text, parse_mode='HTML')
  240. async def stats_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  241. """Handle the /stats command."""
  242. if not self.is_authorized(update.effective_chat.id):
  243. await update.message.reply_text("โŒ Unauthorized access.")
  244. return
  245. # Get current balance for stats
  246. balance = self.client.get_balance()
  247. current_balance = 0
  248. if balance and balance.get('total'):
  249. current_balance = float(balance['total'].get('USDC', 0))
  250. stats_message = self.stats.format_stats_message(current_balance)
  251. await update.message.reply_text(stats_message, parse_mode='HTML')
  252. async def trades_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  253. """Handle the /trades command."""
  254. if not self.is_authorized(update.effective_chat.id):
  255. await update.message.reply_text("โŒ Unauthorized access.")
  256. return
  257. recent_trades = self.stats.get_recent_trades(10)
  258. if not recent_trades:
  259. await update.message.reply_text("๐Ÿ“ No trades recorded yet.")
  260. return
  261. trades_text = "๐Ÿ”„ <b>Recent Trades</b>\n\n"
  262. for trade in reversed(recent_trades[-5:]): # Show last 5 trades
  263. timestamp = datetime.fromisoformat(trade['timestamp']).strftime('%m/%d %H:%M')
  264. side_emoji = "๐ŸŸข" if trade['side'] == 'buy' else "๐Ÿ”ด"
  265. trades_text += f"{side_emoji} <b>{trade['side'].upper()}</b> {trade['amount']} {trade['symbol']}\n"
  266. trades_text += f" ๐Ÿ’ฐ ${trade['price']:,.2f} | ๐Ÿ’ต ${trade['value']:,.2f}\n"
  267. trades_text += f" ๐Ÿ“… {timestamp}\n\n"
  268. await update.message.reply_text(trades_text, parse_mode='HTML')
  269. async def balance_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  270. """Handle the /balance command."""
  271. if not self.is_authorized(update.effective_chat.id):
  272. await update.message.reply_text("โŒ Unauthorized access.")
  273. return
  274. balance = self.client.get_balance()
  275. if balance:
  276. balance_text = "๐Ÿ’ฐ <b>Account Balance</b>\n\n"
  277. # CCXT balance structure includes 'free', 'used', and 'total'
  278. total_balance = balance.get('total', {})
  279. free_balance = balance.get('free', {})
  280. used_balance = balance.get('used', {})
  281. if total_balance:
  282. total_value = 0
  283. available_value = 0
  284. # Display individual assets
  285. for asset, amount in total_balance.items():
  286. if float(amount) > 0:
  287. free_amount = float(free_balance.get(asset, 0))
  288. used_amount = float(used_balance.get(asset, 0))
  289. balance_text += f"๐Ÿ’ต <b>{asset}:</b>\n"
  290. balance_text += f" ๐Ÿ“Š Total: {amount}\n"
  291. balance_text += f" โœ… Available: {free_amount}\n"
  292. if used_amount > 0:
  293. balance_text += f" ๐Ÿ”’ In Use: {used_amount}\n"
  294. balance_text += "\n"
  295. # Calculate totals for USDC (main trading currency)
  296. if asset == 'USDC':
  297. total_value += float(amount)
  298. available_value += free_amount
  299. # Summary section
  300. balance_text += f"๐Ÿ’ผ <b>Portfolio Summary:</b>\n"
  301. balance_text += f" ๐Ÿ’ฐ Total Value: ${total_value:,.2f}\n"
  302. balance_text += f" ๐Ÿš€ Available for Trading: ${available_value:,.2f}\n"
  303. if total_value - available_value > 0:
  304. balance_text += f" ๐Ÿ”’ In Active Use: ${total_value - available_value:,.2f}\n"
  305. # Add P&L summary
  306. basic_stats = self.stats.get_basic_stats()
  307. if basic_stats['initial_balance'] > 0:
  308. pnl = total_value - basic_stats['initial_balance']
  309. pnl_percent = (pnl / basic_stats['initial_balance']) * 100
  310. balance_text += f"\n๐Ÿ“Š <b>Performance:</b>\n"
  311. balance_text += f" ๐Ÿ’ต P&L: ${pnl:,.2f} ({pnl_percent:+.2f}%)\n"
  312. balance_text += f" ๐Ÿ“ˆ Initial: ${basic_stats['initial_balance']:,.2f}"
  313. else:
  314. balance_text += "๐Ÿ“ญ No balance data available"
  315. else:
  316. balance_text = "โŒ Could not fetch balance data"
  317. await update.message.reply_text(balance_text, parse_mode='HTML')
  318. async def positions_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  319. """Handle the /positions command."""
  320. if not self.is_authorized(update.effective_chat.id):
  321. await update.message.reply_text("โŒ Unauthorized access.")
  322. return
  323. positions = self.client.get_positions()
  324. if positions is not None: # Successfully fetched (could be empty list)
  325. positions_text = "๐Ÿ“ˆ <b>Open Positions</b>\n\n"
  326. # Filter for actual open positions
  327. open_positions = [p for p in positions if float(p.get('contracts', 0)) != 0]
  328. if open_positions:
  329. total_unrealized = 0
  330. total_position_value = 0
  331. for position in open_positions:
  332. symbol = position.get('symbol', 'Unknown')
  333. contracts = float(position.get('contracts', 0))
  334. unrealized_pnl = float(position.get('unrealizedPnl', 0))
  335. entry_price = float(position.get('entryPx', 0))
  336. # Calculate position value and P&L percentage
  337. position_value = abs(contracts) * entry_price
  338. pnl_percentage = (unrealized_pnl / position_value * 100) if position_value > 0 else 0
  339. pnl_emoji = "๐ŸŸข" if unrealized_pnl >= 0 else "๐Ÿ”ด"
  340. # Extract token name for cleaner display
  341. token = symbol.split('/')[0] if '/' in symbol else symbol
  342. position_type = "LONG" if contracts > 0 else "SHORT"
  343. positions_text += f"๐Ÿ“Š <b>{token}</b> ({position_type})\n"
  344. positions_text += f" ๐Ÿ“ Size: {abs(contracts):.6f} {token}\n"
  345. positions_text += f" ๐Ÿ’ฐ Entry: ${entry_price:,.2f}\n"
  346. positions_text += f" ๐Ÿ’ต Value: ${position_value:,.2f}\n"
  347. positions_text += f" {pnl_emoji} P&L: ${unrealized_pnl:,.2f} ({pnl_percentage:+.2f}%)\n\n"
  348. total_unrealized += unrealized_pnl
  349. total_position_value += position_value
  350. # Calculate overall P&L percentage
  351. total_pnl_percentage = (total_unrealized / total_position_value * 100) if total_position_value > 0 else 0
  352. total_pnl_emoji = "๐ŸŸข" if total_unrealized >= 0 else "๐Ÿ”ด"
  353. positions_text += f"๐Ÿ’ผ <b>Total Portfolio:</b>\n"
  354. positions_text += f" ๐Ÿ’ต Total Value: ${total_position_value:,.2f}\n"
  355. positions_text += f" {total_pnl_emoji} Total P&L: ${total_unrealized:,.2f} ({total_pnl_percentage:+.2f}%)"
  356. else:
  357. positions_text += "๐Ÿ“ญ <b>No open positions currently</b>\n\n"
  358. positions_text += "๐Ÿš€ Ready to start trading!\n"
  359. positions_text += "Use /buy or /sell commands to open positions."
  360. else:
  361. # Actual API error
  362. positions_text = "โŒ <b>Could not fetch positions data</b>\n\n"
  363. positions_text += "๐Ÿ”„ Please try again in a moment.\n"
  364. positions_text += "If the issue persists, check your connection."
  365. await update.message.reply_text(positions_text, parse_mode='HTML')
  366. async def orders_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  367. """Handle the /orders command with optional token filter."""
  368. if not self.is_authorized(update.effective_chat.id):
  369. await update.message.reply_text("โŒ Unauthorized access.")
  370. return
  371. # Check if token filter is provided
  372. token_filter = None
  373. if context.args and len(context.args) >= 1:
  374. token_filter = context.args[0].upper()
  375. orders = self.client.get_open_orders()
  376. if orders is not None: # Successfully fetched (could be empty list)
  377. if token_filter:
  378. orders_text = f"๐Ÿ“‹ <b>Open Orders - {token_filter}</b>\n\n"
  379. # Filter orders for specific token
  380. target_symbol = f"{token_filter}/USDC:USDC"
  381. filtered_orders = [order for order in orders if order.get('symbol') == target_symbol]
  382. else:
  383. orders_text = "๐Ÿ“‹ <b>All Open Orders</b>\n\n"
  384. filtered_orders = orders
  385. if filtered_orders and len(filtered_orders) > 0:
  386. for order in filtered_orders:
  387. symbol = order.get('symbol', 'Unknown')
  388. side = order.get('side', 'Unknown')
  389. amount = order.get('amount', 0)
  390. price = order.get('price', 0)
  391. order_id = order.get('id', 'Unknown')
  392. # Extract token from symbol for display
  393. token = symbol.split('/')[0] if '/' in symbol else symbol
  394. side_emoji = "๐ŸŸข" if side.lower() == 'buy' else "๐Ÿ”ด"
  395. orders_text += f"{side_emoji} <b>{token}</b>\n"
  396. orders_text += f" ๐Ÿ“Š {side.upper()} {amount} @ ${price:,.2f}\n"
  397. orders_text += f" ๐Ÿ’ต Value: ${float(amount) * float(price):,.2f}\n"
  398. orders_text += f" ๐Ÿ”‘ ID: <code>{order_id}</code>\n\n"
  399. # Add helpful commands
  400. if token_filter:
  401. orders_text += f"๐Ÿ’ก <b>Quick Actions:</b>\n"
  402. orders_text += f"โ€ข <code>/coo {token_filter}</code> - Cancel all {token_filter} orders\n"
  403. orders_text += f"โ€ข <code>/orders</code> - View all orders"
  404. else:
  405. orders_text += f"๐Ÿ’ก <b>Filter by token:</b> <code>/orders BTC</code>, <code>/orders ETH</code>"
  406. else:
  407. if token_filter:
  408. orders_text += f"๐Ÿ“ญ <b>No open orders for {token_filter}</b>\n\n"
  409. orders_text += f"๐Ÿ’ก No pending {token_filter} orders found.\n"
  410. orders_text += f"Use <code>/long {token_filter} 100</code> or <code>/short {token_filter} 100</code> to create new orders."
  411. else:
  412. orders_text += "๐Ÿ“ญ <b>No open orders currently</b>\n\n"
  413. orders_text += "๐Ÿ’ก All clear! No pending orders.\n"
  414. orders_text += "Use /long or /short commands to place new orders."
  415. else:
  416. # Actual API error
  417. orders_text = "โŒ <b>Could not fetch orders data</b>\n\n"
  418. orders_text += "๐Ÿ”„ Please try again in a moment.\n"
  419. orders_text += "If the issue persists, check your connection."
  420. await update.message.reply_text(orders_text, parse_mode='HTML')
  421. async def market_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  422. """Handle the /market command."""
  423. if not self.is_authorized(update.effective_chat.id):
  424. await update.message.reply_text("โŒ Unauthorized access.")
  425. return
  426. # Check if token is provided as argument
  427. if context.args and len(context.args) >= 1:
  428. token = context.args[0].upper()
  429. else:
  430. token = Config.DEFAULT_TRADING_TOKEN
  431. # Convert token to full symbol format for API
  432. symbol = f"{token}/USDC:USDC"
  433. market_data = self.client.get_market_data(symbol)
  434. if market_data and market_data.get('ticker'):
  435. try:
  436. ticker = market_data['ticker']
  437. orderbook = market_data.get('orderbook', {})
  438. # Safely extract ticker data with fallbacks
  439. current_price = float(ticker.get('last') or 0)
  440. high_24h = float(ticker.get('high') or 0)
  441. low_24h = float(ticker.get('low') or 0)
  442. volume_24h = ticker.get('baseVolume') or ticker.get('volume') or 'N/A'
  443. market_text = f"๐Ÿ“Š <b>Market Data - {token}</b>\n\n"
  444. if current_price > 0:
  445. market_text += f"๐Ÿ’ต <b>Current Price:</b> ${current_price:,.2f}\n"
  446. else:
  447. market_text += f"๐Ÿ’ต <b>Current Price:</b> N/A\n"
  448. if high_24h > 0:
  449. market_text += f"๐Ÿ“ˆ <b>24h High:</b> ${high_24h:,.2f}\n"
  450. else:
  451. market_text += f"๐Ÿ“ˆ <b>24h High:</b> N/A\n"
  452. if low_24h > 0:
  453. market_text += f"๐Ÿ“‰ <b>24h Low:</b> ${low_24h:,.2f}\n"
  454. else:
  455. market_text += f"๐Ÿ“‰ <b>24h Low:</b> N/A\n"
  456. market_text += f"๐Ÿ“Š <b>24h Volume:</b> {volume_24h}\n\n"
  457. # Handle orderbook data safely
  458. if orderbook and orderbook.get('bids') and orderbook.get('asks'):
  459. try:
  460. bids = orderbook.get('bids', [])
  461. asks = orderbook.get('asks', [])
  462. if bids and asks and len(bids) > 0 and len(asks) > 0:
  463. best_bid = float(bids[0][0]) if bids[0][0] else 0
  464. best_ask = float(asks[0][0]) if asks[0][0] else 0
  465. if best_bid > 0 and best_ask > 0:
  466. spread = best_ask - best_bid
  467. spread_percent = (spread / best_ask * 100) if best_ask > 0 else 0
  468. market_text += f"๐ŸŸข <b>Best Bid:</b> ${best_bid:,.2f}\n"
  469. market_text += f"๐Ÿ”ด <b>Best Ask:</b> ${best_ask:,.2f}\n"
  470. market_text += f"๐Ÿ“ <b>Spread:</b> ${spread:.2f} ({spread_percent:.3f}%)\n"
  471. else:
  472. market_text += f"๐Ÿ“‹ <b>Orderbook:</b> Data unavailable\n"
  473. else:
  474. market_text += f"๐Ÿ“‹ <b>Orderbook:</b> No orders available\n"
  475. except (IndexError, ValueError, TypeError) as e:
  476. market_text += f"๐Ÿ“‹ <b>Orderbook:</b> Error parsing data\n"
  477. else:
  478. market_text += f"๐Ÿ“‹ <b>Orderbook:</b> Not available\n"
  479. # Add usage hint
  480. market_text += f"\n๐Ÿ’ก <b>Usage:</b> <code>/market {token}</code> or <code>/market</code> for default"
  481. except (ValueError, TypeError) as e:
  482. market_text = f"โŒ <b>Error parsing market data</b>\n\n"
  483. market_text += f"๐Ÿ”ง Raw data received but couldn't parse values.\n"
  484. market_text += f"๐Ÿ“ž Please try again or contact support if this persists."
  485. else:
  486. market_text = f"โŒ <b>Could not fetch market data for {token}</b>\n\n"
  487. market_text += f"๐Ÿ”„ Please try again in a moment.\n"
  488. market_text += f"๐ŸŒ Check your network connection.\n"
  489. market_text += f"๐Ÿ“ก API may be temporarily unavailable.\n\n"
  490. market_text += f"๐Ÿ’ก <b>Usage:</b> <code>/market BTC</code>, <code>/market ETH</code>, <code>/market SOL</code>, etc."
  491. await update.message.reply_text(market_text, parse_mode='HTML')
  492. async def price_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  493. """Handle the /price command."""
  494. if not self.is_authorized(update.effective_chat.id):
  495. await update.message.reply_text("โŒ Unauthorized access.")
  496. return
  497. # Check if token is provided as argument
  498. if context.args and len(context.args) >= 1:
  499. token = context.args[0].upper()
  500. else:
  501. token = Config.DEFAULT_TRADING_TOKEN
  502. # Convert token to full symbol format for API
  503. symbol = f"{token}/USDC:USDC"
  504. market_data = self.client.get_market_data(symbol)
  505. if market_data and market_data.get('ticker'):
  506. try:
  507. ticker = market_data['ticker']
  508. price_value = ticker.get('last')
  509. if price_value is not None:
  510. price = float(price_value)
  511. price_text = f"๐Ÿ’ต <b>{token}</b>: ${price:,.2f}"
  512. # Add timestamp
  513. timestamp = datetime.now().strftime('%H:%M:%S')
  514. price_text += f"\nโฐ <i>Updated: {timestamp}</i>"
  515. # Add usage hint
  516. price_text += f"\n๐Ÿ’ก <i>Usage: </i><code>/price {symbol}</code><i> or </i><code>/price</code><i> for default</i>"
  517. else:
  518. price_text = f"๐Ÿ’ต <b>{symbol}</b>: Price not available\nโš ๏ธ <i>Data temporarily unavailable</i>"
  519. except (ValueError, TypeError) as e:
  520. price_text = f"โŒ <b>Error parsing price for {symbol}</b>\n๐Ÿ”ง <i>Please try again</i>"
  521. else:
  522. price_text = f"โŒ <b>Could not fetch price for {symbol}</b>\n๐Ÿ”„ <i>Please try again in a moment</i>\n\n"
  523. price_text += f"๐Ÿ’ก <b>Usage:</b> <code>/price BTC</code>, <code>/price ETH</code>, <code>/price SOL</code>, etc."
  524. await update.message.reply_text(price_text, parse_mode='HTML')
  525. async def button_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  526. """Handle inline keyboard button presses."""
  527. query = update.callback_query
  528. await query.answer()
  529. if not self.is_authorized(query.message.chat_id):
  530. await query.edit_message_text("โŒ Unauthorized access.")
  531. return
  532. callback_data = query.data
  533. # Handle trading confirmations
  534. if callback_data.startswith('confirm_long_'):
  535. parts = callback_data.split('_')
  536. token = parts[2]
  537. usdc_amount = float(parts[3])
  538. price = float(parts[4])
  539. is_limit = len(parts) > 5 and parts[5] == 'limit'
  540. await self._execute_long_order(query, token, usdc_amount, price, is_limit)
  541. return
  542. elif callback_data.startswith('confirm_short_'):
  543. parts = callback_data.split('_')
  544. token = parts[2]
  545. usdc_amount = float(parts[3])
  546. price = float(parts[4])
  547. is_limit = len(parts) > 5 and parts[5] == 'limit'
  548. await self._execute_short_order(query, token, usdc_amount, price, is_limit)
  549. return
  550. elif callback_data.startswith('confirm_exit_'):
  551. parts = callback_data.split('_')
  552. token = parts[2]
  553. exit_side = parts[3]
  554. contracts = float(parts[4])
  555. price = float(parts[5])
  556. await self._execute_exit_order(query, token, exit_side, contracts, price)
  557. return
  558. elif callback_data.startswith('confirm_coo_'):
  559. parts = callback_data.split('_')
  560. token = parts[2]
  561. await self._execute_coo(query, token)
  562. return
  563. elif callback_data.startswith('confirm_sl_'):
  564. parts = callback_data.split('_')
  565. token = parts[2]
  566. exit_side = parts[3]
  567. contracts = float(parts[4])
  568. price = float(parts[5])
  569. await self._execute_sl_order(query, token, exit_side, contracts, price)
  570. return
  571. elif callback_data.startswith('confirm_tp_'):
  572. parts = callback_data.split('_')
  573. token = parts[2]
  574. exit_side = parts[3]
  575. contracts = float(parts[4])
  576. price = float(parts[5])
  577. await self._execute_tp_order(query, token, exit_side, contracts, price)
  578. return
  579. elif callback_data == 'cancel_order':
  580. await query.edit_message_text("โŒ Order cancelled.")
  581. return
  582. # Create a fake update object for reusing command handlers
  583. fake_update = Update(
  584. update_id=update.update_id,
  585. message=query.message,
  586. callback_query=query
  587. )
  588. # Handle regular button callbacks
  589. if callback_data == "balance":
  590. await self.balance_command(fake_update, context)
  591. elif callback_data == "stats":
  592. await self.stats_command(fake_update, context)
  593. elif callback_data == "positions":
  594. await self.positions_command(fake_update, context)
  595. elif callback_data == "orders":
  596. await self.orders_command(fake_update, context)
  597. elif callback_data == "market":
  598. await self.market_command(fake_update, context)
  599. elif callback_data == "price":
  600. await self.price_command(fake_update, context)
  601. elif callback_data == "trades":
  602. await self.trades_command(fake_update, context)
  603. elif callback_data == "help":
  604. await self.help_command(fake_update, context)
  605. async def _execute_long_order(self, query, token: str, usdc_amount: float, price: float, is_limit: bool):
  606. """Execute a long order."""
  607. symbol = f"{token}/USDC:USDC"
  608. try:
  609. await query.edit_message_text("โณ Opening long position...")
  610. # Calculate token amount based on USDC value and price
  611. token_amount = usdc_amount / price
  612. # Place order (limit or market)
  613. if is_limit:
  614. order = self.client.place_limit_order(symbol, 'buy', token_amount, price)
  615. else:
  616. order = self.client.place_market_order(symbol, 'buy', token_amount)
  617. if order:
  618. # Record the trade in stats
  619. order_id = order.get('id', 'N/A')
  620. actual_price = order.get('average', price) # Use actual fill price if available
  621. self.stats.record_trade(symbol, 'buy', token_amount, actual_price, order_id)
  622. success_message = f"""
  623. โœ… <b>Long Position {'Placed' if is_limit else 'Opened'} Successfully!</b>
  624. ๐Ÿ“Š <b>Order Details:</b>
  625. โ€ข Token: {token}
  626. โ€ข Direction: LONG (Buy)
  627. โ€ข Amount: {token_amount:.6f} {token}
  628. โ€ข Price: ${price:,.2f}
  629. โ€ข USDC Value: ~${usdc_amount:,.2f}
  630. โ€ข Order Type: {'Limit' if is_limit else 'Market'} Order
  631. โ€ข Order ID: <code>{order_id}</code>
  632. ๐Ÿš€ Your {'limit order has been placed' if is_limit else 'long position is now active'}!
  633. """
  634. await query.edit_message_text(success_message, parse_mode='HTML')
  635. logger.info(f"Long {'limit order placed' if is_limit else 'position opened'}: {token_amount:.6f} {token} @ ${price} ({'Limit' if is_limit else 'Market'})")
  636. else:
  637. await query.edit_message_text(f"โŒ Failed to {'place limit order' if is_limit else 'open long position'}. Please try again.")
  638. except Exception as e:
  639. error_message = f"โŒ Error {'placing limit order' if is_limit else 'opening long position'}: {str(e)}"
  640. await query.edit_message_text(error_message)
  641. logger.error(f"Error in long order: {e}")
  642. async def _execute_short_order(self, query, token: str, usdc_amount: float, price: float, is_limit: bool):
  643. """Execute a short order."""
  644. symbol = f"{token}/USDC:USDC"
  645. try:
  646. await query.edit_message_text("โณ Opening short position...")
  647. # Calculate token amount based on USDC value and price
  648. token_amount = usdc_amount / price
  649. # Place order (limit or market)
  650. if is_limit:
  651. order = self.client.place_limit_order(symbol, 'sell', token_amount, price)
  652. else:
  653. order = self.client.place_market_order(symbol, 'sell', token_amount)
  654. if order:
  655. # Record the trade in stats
  656. order_id = order.get('id', 'N/A')
  657. actual_price = order.get('average', price) # Use actual fill price if available
  658. self.stats.record_trade(symbol, 'sell', token_amount, actual_price, order_id)
  659. success_message = f"""
  660. โœ… <b>Short Position {'Placed' if is_limit else 'Opened'} Successfully!</b>
  661. ๐Ÿ“Š <b>Order Details:</b>
  662. โ€ข Token: {token}
  663. โ€ข Direction: SHORT (Sell)
  664. โ€ข Amount: {token_amount:.6f} {token}
  665. โ€ข Price: ${price:,.2f}
  666. โ€ข USDC Value: ~${usdc_amount:,.2f}
  667. โ€ข Order Type: {'Limit' if is_limit else 'Market'} Order
  668. โ€ข Order ID: <code>{order_id}</code>
  669. ๐Ÿ“‰ Your {'limit order has been placed' if is_limit else 'short position is now active'}!
  670. """
  671. await query.edit_message_text(success_message, parse_mode='HTML')
  672. logger.info(f"Short {'limit order placed' if is_limit else 'position opened'}: {token_amount:.6f} {token} @ ${price} ({'Limit' if is_limit else 'Market'})")
  673. else:
  674. await query.edit_message_text(f"โŒ Failed to {'place limit order' if is_limit else 'open short position'}. Please try again.")
  675. except Exception as e:
  676. error_message = f"โŒ Error {'placing limit order' if is_limit else 'opening short position'}: {str(e)}"
  677. await query.edit_message_text(error_message)
  678. logger.error(f"Error in short order: {e}")
  679. async def _execute_exit_order(self, query, token: str, exit_side: str, contracts: float, price: float):
  680. """Execute an exit order."""
  681. symbol = f"{token}/USDC:USDC"
  682. try:
  683. await query.edit_message_text("โณ Closing position...")
  684. # Place market order to close position
  685. order = self.client.place_market_order(symbol, exit_side, contracts)
  686. if order:
  687. # Record the trade in stats
  688. order_id = order.get('id', 'N/A')
  689. actual_price = order.get('average', price) # Use actual fill price if available
  690. self.stats.record_trade(symbol, exit_side, contracts, actual_price, order_id)
  691. position_type = "LONG" if exit_side == "sell" else "SHORT"
  692. success_message = f"""
  693. โœ… <b>Position Closed Successfully!</b>
  694. ๐Ÿ“Š <b>Exit Details:</b>
  695. โ€ข Token: {token}
  696. โ€ข Position Closed: {position_type}
  697. โ€ข Exit Side: {exit_side.upper()}
  698. โ€ข Amount: {contracts} {token}
  699. โ€ข Est. Price: ~${price:,.2f}
  700. โ€ข Order Type: Market Order
  701. โ€ข Order ID: <code>{order_id}</code>
  702. ๐ŸŽฏ <b>Position Summary:</b>
  703. โ€ข Status: CLOSED
  704. โ€ข Exit Value: ~${contracts * price:,.2f}
  705. ๐Ÿ“Š Use /stats to see updated performance metrics.
  706. """
  707. await query.edit_message_text(success_message, parse_mode='HTML')
  708. logger.info(f"Position closed: {exit_side} {contracts} {token} @ ~${price}")
  709. else:
  710. await query.edit_message_text("โŒ Failed to close position. Please try again.")
  711. except Exception as e:
  712. error_message = f"โŒ Error closing position: {str(e)}"
  713. await query.edit_message_text(error_message)
  714. logger.error(f"Error closing position: {e}")
  715. async def _execute_coo(self, query, token: str):
  716. """Execute cancel open orders for a specific token."""
  717. symbol = f"{token}/USDC:USDC"
  718. try:
  719. await query.edit_message_text("โณ Cancelling all orders...")
  720. # Get current orders for this token
  721. all_orders = self.client.get_open_orders()
  722. if all_orders is None:
  723. await query.edit_message_text(f"โŒ Could not fetch orders to cancel {token} orders")
  724. return
  725. # Filter orders for the specific token
  726. token_orders = [order for order in all_orders if order.get('symbol') == symbol]
  727. if not token_orders:
  728. await query.edit_message_text(f"๐Ÿ“ญ No open orders found for {token}")
  729. return
  730. # Cancel each order
  731. cancelled_orders = []
  732. failed_orders = []
  733. for order in token_orders:
  734. order_id = order.get('id')
  735. if order_id:
  736. try:
  737. success = self.client.cancel_order(order_id, symbol)
  738. if success:
  739. cancelled_orders.append(order)
  740. else:
  741. failed_orders.append(order)
  742. except Exception as e:
  743. logger.error(f"Failed to cancel order {order_id}: {e}")
  744. failed_orders.append(order)
  745. # Create result message
  746. result_message = f"""
  747. โœ… <b>Cancel Orders Results</b>
  748. ๐Ÿ“Š <b>Summary:</b>
  749. โ€ข Token: {token}
  750. โ€ข Cancelled: {len(cancelled_orders)} orders
  751. โ€ข Failed: {len(failed_orders)} orders
  752. โ€ข Total Attempted: {len(token_orders)} orders
  753. """
  754. if cancelled_orders:
  755. result_message += f"\n๐Ÿ—‘๏ธ <b>Successfully Cancelled:</b>\n"
  756. for order in cancelled_orders:
  757. side = order.get('side', 'Unknown')
  758. amount = order.get('amount', 0)
  759. price = order.get('price', 0)
  760. side_emoji = "๐ŸŸข" if side.lower() == 'buy' else "๐Ÿ”ด"
  761. result_message += f"{side_emoji} {side.upper()} {amount} @ ${price:,.2f}\n"
  762. if failed_orders:
  763. result_message += f"\nโŒ <b>Failed to Cancel:</b>\n"
  764. for order in failed_orders:
  765. side = order.get('side', 'Unknown')
  766. amount = order.get('amount', 0)
  767. price = order.get('price', 0)
  768. order_id = order.get('id', 'Unknown')
  769. side_emoji = "๐ŸŸข" if side.lower() == 'buy' else "๐Ÿ”ด"
  770. result_message += f"{side_emoji} {side.upper()} {amount} @ ${price:,.2f} (ID: {order_id})\n"
  771. if len(cancelled_orders) == len(token_orders):
  772. result_message += f"\n๐ŸŽ‰ All {token} orders successfully cancelled!"
  773. elif len(cancelled_orders) > 0:
  774. result_message += f"\nโš ๏ธ Some orders cancelled. Check failed orders above."
  775. else:
  776. result_message += f"\nโŒ Could not cancel any {token} orders."
  777. await query.edit_message_text(result_message, parse_mode='HTML')
  778. logger.info(f"COO executed for {token}: {len(cancelled_orders)}/{len(token_orders)} orders cancelled")
  779. except Exception as e:
  780. error_message = f"โŒ Error cancelling {token} orders: {str(e)}"
  781. await query.edit_message_text(error_message)
  782. logger.error(f"Error in COO execution: {e}")
  783. async def _execute_sl_order(self, query, token: str, exit_side: str, contracts: float, price: float):
  784. """Execute a stop loss order."""
  785. symbol = f"{token}/USDC:USDC"
  786. try:
  787. await query.edit_message_text("โณ Setting stop loss...")
  788. # Place stop loss order
  789. order = self.client.place_stop_loss_order(symbol, exit_side, contracts, price)
  790. if order:
  791. # Record the trade in stats
  792. order_id = order.get('id', 'N/A')
  793. actual_price = order.get('average', price) # Use actual fill price if available
  794. self.stats.record_trade(symbol, exit_side, contracts, actual_price, order_id)
  795. position_type = "LONG" if exit_side == "sell" else "SHORT"
  796. success_message = f"""
  797. โœ… <b>Stop Loss Order Set Successfully!</b>
  798. ๐Ÿ“Š <b>Stop Loss Details:</b>
  799. โ€ข Token: {token}
  800. โ€ข Position: {position_type}
  801. โ€ข Size: {contracts} contracts
  802. โ€ข Stop Price: ${price:,.2f}
  803. โ€ข Action: {exit_side.upper()} (Close {position_type})
  804. โ€ข Amount: {contracts} {token}
  805. โ€ข Order Type: Limit Order
  806. โ€ข Order ID: <code>{order_id}</code>
  807. ๐ŸŽฏ <b>Stop Loss Execution:</b>
  808. โ€ข Status: SET
  809. โ€ข Exit Value: ~${contracts * price:,.2f}
  810. ๐Ÿ“Š Use /stats to see updated performance metrics.
  811. """
  812. await query.edit_message_text(success_message, parse_mode='HTML')
  813. logger.info(f"Stop loss set: {exit_side} {contracts} {token} @ ${price}")
  814. else:
  815. await query.edit_message_text("โŒ Failed to set stop loss. Please try again.")
  816. except Exception as e:
  817. error_message = f"โŒ Error setting stop loss: {str(e)}"
  818. await query.edit_message_text(error_message)
  819. logger.error(f"Error setting stop loss: {e}")
  820. async def _execute_tp_order(self, query, token: str, exit_side: str, contracts: float, price: float):
  821. """Execute a take profit order."""
  822. symbol = f"{token}/USDC:USDC"
  823. try:
  824. await query.edit_message_text("โณ Setting take profit...")
  825. # Place take profit order
  826. order = self.client.place_take_profit_order(symbol, exit_side, contracts, price)
  827. if order:
  828. # Record the trade in stats
  829. order_id = order.get('id', 'N/A')
  830. actual_price = order.get('average', price) # Use actual fill price if available
  831. self.stats.record_trade(symbol, exit_side, contracts, actual_price, order_id)
  832. position_type = "LONG" if exit_side == "sell" else "SHORT"
  833. success_message = f"""
  834. โœ… <b>Take Profit Order Set Successfully!</b>
  835. ๐Ÿ“Š <b>Take Profit Details:</b>
  836. โ€ข Token: {token}
  837. โ€ข Position: {position_type}
  838. โ€ข Size: {contracts} contracts
  839. โ€ข Target Price: ${price:,.2f}
  840. โ€ข Action: {exit_side.upper()} (Close {position_type})
  841. โ€ข Amount: {contracts} {token}
  842. โ€ข Order Type: Limit Order
  843. โ€ข Order ID: <code>{order_id}</code>
  844. ๐ŸŽฏ <b>Take Profit Execution:</b>
  845. โ€ข Status: SET
  846. โ€ข Exit Value: ~${contracts * price:,.2f}
  847. ๐Ÿ“Š Use /stats to see updated performance metrics.
  848. """
  849. await query.edit_message_text(success_message, parse_mode='HTML')
  850. logger.info(f"Take profit set: {exit_side} {contracts} {token} @ ${price}")
  851. else:
  852. await query.edit_message_text("โŒ Failed to set take profit. Please try again.")
  853. except Exception as e:
  854. error_message = f"โŒ Error setting take profit: {str(e)}"
  855. await query.edit_message_text(error_message)
  856. logger.error(f"Error setting take profit: {e}")
  857. async def unknown_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  858. """Handle unknown commands."""
  859. if not self.is_authorized(update.effective_chat.id):
  860. await update.message.reply_text("โŒ Unauthorized access.")
  861. return
  862. await update.message.reply_text(
  863. "โ“ Unknown command. Use /help to see available commands or tap the buttons in /start."
  864. )
  865. def setup_handlers(self):
  866. """Set up command handlers for the bot."""
  867. if not self.application:
  868. return
  869. # Command handlers
  870. self.application.add_handler(CommandHandler("start", self.start_command))
  871. self.application.add_handler(CommandHandler("help", self.help_command))
  872. self.application.add_handler(CommandHandler("balance", self.balance_command))
  873. self.application.add_handler(CommandHandler("positions", self.positions_command))
  874. self.application.add_handler(CommandHandler("orders", self.orders_command))
  875. self.application.add_handler(CommandHandler("market", self.market_command))
  876. self.application.add_handler(CommandHandler("price", self.price_command))
  877. self.application.add_handler(CommandHandler("stats", self.stats_command))
  878. self.application.add_handler(CommandHandler("trades", self.trades_command))
  879. self.application.add_handler(CommandHandler("long", self.long_command))
  880. self.application.add_handler(CommandHandler("short", self.short_command))
  881. self.application.add_handler(CommandHandler("exit", self.exit_command))
  882. self.application.add_handler(CommandHandler("coo", self.coo_command))
  883. self.application.add_handler(CommandHandler("sl", self.sl_command))
  884. self.application.add_handler(CommandHandler("tp", self.tp_command))
  885. self.application.add_handler(CommandHandler("monitoring", self.monitoring_command))
  886. self.application.add_handler(CommandHandler("alarm", self.alarm_command))
  887. self.application.add_handler(CommandHandler("logs", self.logs_command))
  888. self.application.add_handler(CommandHandler("performance", self.performance_command))
  889. self.application.add_handler(CommandHandler("daily", self.daily_command))
  890. self.application.add_handler(CommandHandler("weekly", self.weekly_command))
  891. self.application.add_handler(CommandHandler("monthly", self.monthly_command))
  892. # Callback query handler for inline keyboards
  893. self.application.add_handler(CallbackQueryHandler(self.button_callback))
  894. # Handle unknown commands
  895. self.application.add_handler(MessageHandler(filters.COMMAND, self.unknown_command))
  896. async def run(self):
  897. """Run the Telegram bot."""
  898. if not Config.TELEGRAM_BOT_TOKEN:
  899. logger.error("โŒ TELEGRAM_BOT_TOKEN not configured")
  900. return
  901. if not Config.TELEGRAM_CHAT_ID:
  902. logger.error("โŒ TELEGRAM_CHAT_ID not configured")
  903. return
  904. try:
  905. # Create application
  906. self.application = Application.builder().token(Config.TELEGRAM_BOT_TOKEN).build()
  907. # Set up handlers
  908. self.setup_handlers()
  909. logger.info("๐Ÿš€ Starting Telegram trading bot...")
  910. # Initialize the application
  911. await self.application.initialize()
  912. # Send startup notification
  913. await self.send_message(
  914. "๐Ÿค– <b>Manual Trading Bot Started</b>\n\n"
  915. f"โœ… Connected to Hyperliquid {'Testnet' if Config.HYPERLIQUID_TESTNET else 'Mainnet'}\n"
  916. f"๐Ÿ“Š Default Symbol: {Config.DEFAULT_TRADING_TOKEN}\n"
  917. f"๐Ÿ“ฑ Manual trading ready!\n"
  918. f"๐Ÿ”„ Order monitoring: Active ({Config.BOT_HEARTBEAT_SECONDS}s interval)\n"
  919. f"๐Ÿ”„ External trade monitoring: Active\n"
  920. f"๐Ÿ”” Price alarms: Active\n"
  921. f"๐Ÿ“Š Auto stats sync: Enabled\n"
  922. f"๐Ÿ“ Logging: {'File + Console' if Config.LOG_TO_FILE else 'Console only'}\n"
  923. f"โฐ Started at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
  924. "Use /start for quick actions or /help for all commands."
  925. )
  926. # Perform initial log cleanup
  927. try:
  928. cleanup_logs(days_to_keep=30)
  929. logger.info("๐Ÿงน Initial log cleanup completed")
  930. except Exception as e:
  931. logger.warning(f"โš ๏ธ Initial log cleanup failed: {e}")
  932. # Start the application
  933. await self.application.start()
  934. # Start order monitoring
  935. await self.start_order_monitoring()
  936. # Start polling for updates manually
  937. logger.info("๐Ÿ”„ Starting update polling...")
  938. # Get updates in a loop
  939. last_update_id = 0
  940. while True:
  941. try:
  942. # Get updates from Telegram
  943. updates = await self.application.bot.get_updates(
  944. offset=last_update_id + 1,
  945. timeout=30,
  946. allowed_updates=None
  947. )
  948. # Process each update
  949. for update in updates:
  950. last_update_id = update.update_id
  951. # Process the update through the application
  952. await self.application.process_update(update)
  953. except Exception as e:
  954. logger.error(f"Error processing updates: {e}")
  955. await asyncio.sleep(5) # Wait before retrying
  956. except asyncio.CancelledError:
  957. logger.info("๐Ÿ›‘ Bot polling cancelled")
  958. raise
  959. except Exception as e:
  960. logger.error(f"โŒ Error in telegram bot: {e}")
  961. raise
  962. finally:
  963. # Clean shutdown
  964. try:
  965. await self.stop_order_monitoring()
  966. if self.application:
  967. await self.application.stop()
  968. await self.application.shutdown()
  969. except Exception as e:
  970. logger.error(f"Error during shutdown: {e}")
  971. async def long_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  972. """Handle the /long command for opening long positions."""
  973. if not self.is_authorized(update.effective_chat.id):
  974. await update.message.reply_text("โŒ Unauthorized access.")
  975. return
  976. try:
  977. if not context.args or len(context.args) < 2:
  978. await update.message.reply_text(
  979. "โŒ Usage: /long [token] [USDC amount] [price (optional)]\n"
  980. "Examples:\n"
  981. "โ€ข /long BTC 100 - Market order\n"
  982. "โ€ข /long BTC 100 45000 - Limit order at $45,000"
  983. )
  984. return
  985. token = context.args[0].upper()
  986. usdc_amount = float(context.args[1])
  987. # Check if price is provided for limit order
  988. limit_price = None
  989. if len(context.args) >= 3:
  990. limit_price = float(context.args[2])
  991. order_type = "Limit"
  992. order_description = f"at ${limit_price:,.2f}"
  993. else:
  994. order_type = "Market"
  995. order_description = "at current market price"
  996. # Convert token to full symbol format for Hyperliquid
  997. symbol = f"{token}/USDC:USDC"
  998. # Get current market price to calculate amount and for display
  999. market_data = self.client.get_market_data(symbol)
  1000. if not market_data:
  1001. await update.message.reply_text(f"โŒ Could not fetch price for {token}")
  1002. return
  1003. current_price = float(market_data['ticker'].get('last', 0))
  1004. if current_price <= 0:
  1005. await update.message.reply_text(f"โŒ Invalid price for {token}")
  1006. return
  1007. # Calculate token amount based on price (market or limit)
  1008. calculation_price = limit_price if limit_price else current_price
  1009. token_amount = usdc_amount / calculation_price
  1010. # Create confirmation message
  1011. confirmation_text = f"""
  1012. ๐ŸŸข <b>Long Position Confirmation</b>
  1013. ๐Ÿ“Š <b>Order Details:</b>
  1014. โ€ข Token: {token}
  1015. โ€ข Direction: LONG (Buy)
  1016. โ€ข USDC Value: ${usdc_amount:,.2f}
  1017. โ€ข Current Price: ${current_price:,.2f}
  1018. โ€ข Order Type: {order_type} Order
  1019. โ€ข Token Amount: {token_amount:.6f} {token}
  1020. ๐ŸŽฏ <b>Execution:</b>
  1021. โ€ข Will buy {token_amount:.6f} {token} {order_description}
  1022. โ€ข Est. Value: ${token_amount * calculation_price:,.2f}
  1023. โš ๏ธ <b>Are you sure you want to open this long position?</b>
  1024. """
  1025. # Use limit_price for callback if provided, otherwise current_price
  1026. callback_price = limit_price if limit_price else current_price
  1027. callback_data = f"confirm_long_{token}_{usdc_amount}_{callback_price}"
  1028. if limit_price:
  1029. callback_data += "_limit"
  1030. keyboard = [
  1031. [
  1032. InlineKeyboardButton("โœ… Confirm Long", callback_data=callback_data),
  1033. InlineKeyboardButton("โŒ Cancel", callback_data="cancel_order")
  1034. ]
  1035. ]
  1036. reply_markup = InlineKeyboardMarkup(keyboard)
  1037. await update.message.reply_text(confirmation_text, parse_mode='HTML', reply_markup=reply_markup)
  1038. except ValueError:
  1039. await update.message.reply_text("โŒ Invalid USDC amount or price. Please use numbers only.")
  1040. except Exception as e:
  1041. await update.message.reply_text(f"โŒ Error processing long command: {e}")
  1042. async def short_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  1043. """Handle the /short command for opening short positions."""
  1044. if not self.is_authorized(update.effective_chat.id):
  1045. await update.message.reply_text("โŒ Unauthorized access.")
  1046. return
  1047. try:
  1048. if not context.args or len(context.args) < 2:
  1049. await update.message.reply_text(
  1050. "โŒ Usage: /short [token] [USDC amount] [price (optional)]\n"
  1051. "Examples:\n"
  1052. "โ€ข /short BTC 100 - Market order\n"
  1053. "โ€ข /short BTC 100 46000 - Limit order at $46,000"
  1054. )
  1055. return
  1056. token = context.args[0].upper()
  1057. usdc_amount = float(context.args[1])
  1058. # Check if price is provided for limit order
  1059. limit_price = None
  1060. if len(context.args) >= 3:
  1061. limit_price = float(context.args[2])
  1062. order_type = "Limit"
  1063. order_description = f"at ${limit_price:,.2f}"
  1064. else:
  1065. order_type = "Market"
  1066. order_description = "at current market price"
  1067. # Convert token to full symbol format for Hyperliquid
  1068. symbol = f"{token}/USDC:USDC"
  1069. # Get current market price to calculate amount and for display
  1070. market_data = self.client.get_market_data(symbol)
  1071. if not market_data:
  1072. await update.message.reply_text(f"โŒ Could not fetch price for {token}")
  1073. return
  1074. current_price = float(market_data['ticker'].get('last', 0))
  1075. if current_price <= 0:
  1076. await update.message.reply_text(f"โŒ Invalid price for {token}")
  1077. return
  1078. # Calculate token amount based on price (market or limit)
  1079. calculation_price = limit_price if limit_price else current_price
  1080. token_amount = usdc_amount / calculation_price
  1081. # Create confirmation message
  1082. confirmation_text = f"""
  1083. ๐Ÿ”ด <b>Short Position Confirmation</b>
  1084. ๐Ÿ“Š <b>Order Details:</b>
  1085. โ€ข Token: {token}
  1086. โ€ข Direction: SHORT (Sell)
  1087. โ€ข USDC Value: ${usdc_amount:,.2f}
  1088. โ€ข Current Price: ${current_price:,.2f}
  1089. โ€ข Order Type: {order_type} Order
  1090. โ€ข Token Amount: {token_amount:.6f} {token}
  1091. ๐ŸŽฏ <b>Execution:</b>
  1092. โ€ข Will sell {token_amount:.6f} {token} {order_description}
  1093. โ€ข Est. Value: ${token_amount * calculation_price:,.2f}
  1094. โš ๏ธ <b>Are you sure you want to open this short position?</b>
  1095. """
  1096. # Use limit_price for callback if provided, otherwise current_price
  1097. callback_price = limit_price if limit_price else current_price
  1098. callback_data = f"confirm_short_{token}_{usdc_amount}_{callback_price}"
  1099. if limit_price:
  1100. callback_data += "_limit"
  1101. keyboard = [
  1102. [
  1103. InlineKeyboardButton("โœ… Confirm Short", callback_data=callback_data),
  1104. InlineKeyboardButton("โŒ Cancel", callback_data="cancel_order")
  1105. ]
  1106. ]
  1107. reply_markup = InlineKeyboardMarkup(keyboard)
  1108. await update.message.reply_text(confirmation_text, parse_mode='HTML', reply_markup=reply_markup)
  1109. except ValueError:
  1110. await update.message.reply_text("โŒ Invalid USDC amount or price. Please use numbers only.")
  1111. except Exception as e:
  1112. await update.message.reply_text(f"โŒ Error processing short command: {e}")
  1113. async def exit_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  1114. """Handle the /exit command for closing positions."""
  1115. if not self.is_authorized(update.effective_chat.id):
  1116. await update.message.reply_text("โŒ Unauthorized access.")
  1117. return
  1118. try:
  1119. if not context.args or len(context.args) < 1:
  1120. await update.message.reply_text(
  1121. "โŒ Usage: /exit [token]\n"
  1122. "Example: /exit BTC"
  1123. )
  1124. return
  1125. token = context.args[0].upper()
  1126. symbol = f"{token}/USDC:USDC"
  1127. # Get current positions to find the position for this token
  1128. positions = self.client.get_positions()
  1129. if positions is None:
  1130. await update.message.reply_text(f"โŒ Could not fetch positions to check {token} position")
  1131. return
  1132. # Find the position for this token
  1133. current_position = None
  1134. for position in positions:
  1135. if position.get('symbol') == symbol and float(position.get('contracts', 0)) != 0:
  1136. current_position = position
  1137. break
  1138. if not current_position:
  1139. await update.message.reply_text(f"๐Ÿ“ญ No open position found for {token}")
  1140. return
  1141. # Extract position details
  1142. contracts = float(current_position.get('contracts', 0))
  1143. entry_price = float(current_position.get('entryPx', 0))
  1144. unrealized_pnl = float(current_position.get('unrealizedPnl', 0))
  1145. # Determine position direction and exit details
  1146. if contracts > 0:
  1147. position_type = "LONG"
  1148. exit_side = "sell"
  1149. exit_emoji = "๐Ÿ”ด"
  1150. else:
  1151. position_type = "SHORT"
  1152. exit_side = "buy"
  1153. exit_emoji = "๐ŸŸข"
  1154. contracts = abs(contracts) # Make positive for display
  1155. # Get current market price
  1156. market_data = self.client.get_market_data(symbol)
  1157. if not market_data:
  1158. await update.message.reply_text(f"โŒ Could not fetch current price for {token}")
  1159. return
  1160. current_price = float(market_data['ticker'].get('last', 0))
  1161. if current_price <= 0:
  1162. await update.message.reply_text(f"โŒ Invalid current price for {token}")
  1163. return
  1164. # Calculate estimated exit value
  1165. exit_value = contracts * current_price
  1166. # Create confirmation message
  1167. pnl_emoji = "๐ŸŸข" if unrealized_pnl >= 0 else "๐Ÿ”ด"
  1168. confirmation_text = f"""
  1169. {exit_emoji} <b>Exit Position Confirmation</b>
  1170. ๐Ÿ“Š <b>Position Details:</b>
  1171. โ€ข Token: {token}
  1172. โ€ข Position: {position_type}
  1173. โ€ข Size: {contracts} contracts
  1174. โ€ข Entry Price: ${entry_price:,.2f}
  1175. โ€ข Current Price: ${current_price:,.2f}
  1176. โ€ข {pnl_emoji} Unrealized P&L: ${unrealized_pnl:,.2f}
  1177. ๐ŸŽฏ <b>Exit Order:</b>
  1178. โ€ข Action: {exit_side.upper()} (Close {position_type})
  1179. โ€ข Amount: {contracts} {token}
  1180. โ€ข Est. Value: ~${exit_value:,.2f}
  1181. โ€ข Order Type: Market Order
  1182. โš ๏ธ <b>Are you sure you want to close this {position_type} position?</b>
  1183. This will place a market {exit_side} order to close your entire {token} position.
  1184. """
  1185. keyboard = [
  1186. [
  1187. InlineKeyboardButton(f"โœ… Close {position_type}", callback_data=f"confirm_exit_{token}_{exit_side}_{contracts}_{current_price}"),
  1188. InlineKeyboardButton("โŒ Cancel", callback_data="cancel_order")
  1189. ]
  1190. ]
  1191. reply_markup = InlineKeyboardMarkup(keyboard)
  1192. await update.message.reply_text(confirmation_text, parse_mode='HTML', reply_markup=reply_markup)
  1193. except ValueError:
  1194. await update.message.reply_text("โŒ Invalid token format. Please use token symbols like BTC, ETH, etc.")
  1195. except Exception as e:
  1196. await update.message.reply_text(f"โŒ Error processing exit command: {e}")
  1197. async def coo_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  1198. """Handle the /coo (cancel open orders) command for a specific token."""
  1199. if not self.is_authorized(update.effective_chat.id):
  1200. await update.message.reply_text("โŒ Unauthorized access.")
  1201. return
  1202. try:
  1203. if not context.args or len(context.args) < 1:
  1204. await update.message.reply_text(
  1205. "โŒ Usage: /coo [token]\n"
  1206. "Example: /coo BTC\n\n"
  1207. "This command cancels ALL open orders for the specified token."
  1208. )
  1209. return
  1210. token = context.args[0].upper()
  1211. symbol = f"{token}/USDC:USDC"
  1212. # Get current orders for this token
  1213. all_orders = self.client.get_open_orders()
  1214. if all_orders is None:
  1215. await update.message.reply_text(f"โŒ Could not fetch orders to cancel {token} orders")
  1216. return
  1217. # Filter orders for the specific token
  1218. token_orders = [order for order in all_orders if order.get('symbol') == symbol]
  1219. if not token_orders:
  1220. await update.message.reply_text(f"๐Ÿ“ญ No open orders found for {token}")
  1221. return
  1222. # Create confirmation message with order details
  1223. confirmation_text = f"""
  1224. โš ๏ธ <b>Cancel All {token} Orders</b>
  1225. ๐Ÿ“‹ <b>Orders to Cancel:</b>
  1226. """
  1227. total_value = 0
  1228. for order in token_orders:
  1229. side = order.get('side', 'Unknown')
  1230. amount = order.get('amount', 0)
  1231. price = order.get('price', 0)
  1232. order_id = order.get('id', 'Unknown')
  1233. side_emoji = "๐ŸŸข" if side.lower() == 'buy' else "๐Ÿ”ด"
  1234. order_value = float(amount) * float(price)
  1235. total_value += order_value
  1236. confirmation_text += f"{side_emoji} {side.upper()} {amount} @ ${price:,.2f} (${order_value:,.2f})\n"
  1237. confirmation_text += f"""
  1238. ๐Ÿ’ฐ <b>Total Value:</b> ${total_value:,.2f}
  1239. ๐Ÿ”ข <b>Orders Count:</b> {len(token_orders)}
  1240. โš ๏ธ <b>Are you sure you want to cancel ALL {token} orders?</b>
  1241. This action cannot be undone.
  1242. """
  1243. keyboard = [
  1244. [
  1245. InlineKeyboardButton(f"โœ… Cancel All {token}", callback_data=f"confirm_coo_{token}"),
  1246. InlineKeyboardButton("โŒ Keep Orders", callback_data="cancel_order")
  1247. ]
  1248. ]
  1249. reply_markup = InlineKeyboardMarkup(keyboard)
  1250. await update.message.reply_text(confirmation_text, parse_mode='HTML', reply_markup=reply_markup)
  1251. except ValueError:
  1252. await update.message.reply_text("โŒ Invalid token format. Please use token symbols like BTC, ETH, etc.")
  1253. except Exception as e:
  1254. await update.message.reply_text(f"โŒ Error processing cancel orders command: {e}")
  1255. async def sl_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  1256. """Handle the /sl (stop loss) command for setting stop loss orders."""
  1257. if not self.is_authorized(update.effective_chat.id):
  1258. await update.message.reply_text("โŒ Unauthorized access.")
  1259. return
  1260. try:
  1261. if not context.args or len(context.args) < 2:
  1262. await update.message.reply_text(
  1263. "โŒ Usage: /sl [token] [price]\n"
  1264. "Example: /sl BTC 44000\n\n"
  1265. "This creates a stop loss order at the specified price."
  1266. )
  1267. return
  1268. token = context.args[0].upper()
  1269. stop_price = float(context.args[1])
  1270. symbol = f"{token}/USDC:USDC"
  1271. # Get current positions to find the position for this token
  1272. positions = self.client.get_positions()
  1273. if positions is None:
  1274. await update.message.reply_text(f"โŒ Could not fetch positions to check {token} position")
  1275. return
  1276. # Find the position for this token
  1277. current_position = None
  1278. for position in positions:
  1279. if position.get('symbol') == symbol and float(position.get('contracts', 0)) != 0:
  1280. current_position = position
  1281. break
  1282. if not current_position:
  1283. await update.message.reply_text(f"๐Ÿ“ญ No open position found for {token}\n\nYou need an open position to set a stop loss.")
  1284. return
  1285. # Extract position details
  1286. contracts = float(current_position.get('contracts', 0))
  1287. entry_price = float(current_position.get('entryPx', 0))
  1288. unrealized_pnl = float(current_position.get('unrealizedPnl', 0))
  1289. # Determine position direction and validate stop loss price
  1290. if contracts > 0:
  1291. # Long position - stop loss should be below entry price
  1292. position_type = "LONG"
  1293. exit_side = "sell"
  1294. exit_emoji = "๐Ÿ”ด"
  1295. contracts_abs = contracts
  1296. if stop_price >= entry_price:
  1297. await update.message.reply_text(
  1298. f"โš ๏ธ Stop loss price should be BELOW entry price for long positions\n\n"
  1299. f"๐Ÿ“Š Your {token} LONG position:\n"
  1300. f"โ€ข Entry Price: ${entry_price:,.2f}\n"
  1301. f"โ€ข Stop Price: ${stop_price:,.2f} โŒ\n\n"
  1302. f"๐Ÿ’ก Try a lower price like: /sl {token} {entry_price * 0.95:.0f}"
  1303. )
  1304. return
  1305. else:
  1306. # Short position - stop loss should be above entry price
  1307. position_type = "SHORT"
  1308. exit_side = "buy"
  1309. exit_emoji = "๐ŸŸข"
  1310. contracts_abs = abs(contracts)
  1311. if stop_price <= entry_price:
  1312. await update.message.reply_text(
  1313. f"โš ๏ธ Stop loss price should be ABOVE entry price for short positions\n\n"
  1314. f"๐Ÿ“Š Your {token} SHORT position:\n"
  1315. f"โ€ข Entry Price: ${entry_price:,.2f}\n"
  1316. f"โ€ข Stop Price: ${stop_price:,.2f} โŒ\n\n"
  1317. f"๐Ÿ’ก Try a higher price like: /sl {token} {entry_price * 1.05:.0f}"
  1318. )
  1319. return
  1320. # Get current market price for reference
  1321. market_data = self.client.get_market_data(symbol)
  1322. current_price = 0
  1323. if market_data:
  1324. current_price = float(market_data['ticker'].get('last', 0))
  1325. # Calculate estimated P&L at stop loss
  1326. if contracts > 0: # Long position
  1327. pnl_at_stop = (stop_price - entry_price) * contracts_abs
  1328. else: # Short position
  1329. pnl_at_stop = (entry_price - stop_price) * contracts_abs
  1330. # Create confirmation message
  1331. pnl_emoji = "๐ŸŸข" if pnl_at_stop >= 0 else "๐Ÿ”ด"
  1332. confirmation_text = f"""
  1333. ๐Ÿ›‘ <b>Stop Loss Order Confirmation</b>
  1334. ๐Ÿ“Š <b>Position Details:</b>
  1335. โ€ข Token: {token}
  1336. โ€ข Position: {position_type}
  1337. โ€ข Size: {contracts_abs} contracts
  1338. โ€ข Entry Price: ${entry_price:,.2f}
  1339. โ€ข Current Price: ${current_price:,.2f}
  1340. ๐ŸŽฏ <b>Stop Loss Order:</b>
  1341. โ€ข Stop Price: ${stop_price:,.2f}
  1342. โ€ข Action: {exit_side.upper()} (Close {position_type})
  1343. โ€ข Amount: {contracts_abs} {token}
  1344. โ€ข Order Type: Limit Order
  1345. โ€ข {pnl_emoji} Est. P&L: ${pnl_at_stop:,.2f}
  1346. โš ๏ธ <b>Are you sure you want to set this stop loss?</b>
  1347. This will place a limit {exit_side} order at ${stop_price:,.2f} to protect your {position_type} position.
  1348. """
  1349. keyboard = [
  1350. [
  1351. InlineKeyboardButton(f"โœ… Set Stop Loss", callback_data=f"confirm_sl_{token}_{exit_side}_{contracts_abs}_{stop_price}"),
  1352. InlineKeyboardButton("โŒ Cancel", callback_data="cancel_order")
  1353. ]
  1354. ]
  1355. reply_markup = InlineKeyboardMarkup(keyboard)
  1356. await update.message.reply_text(confirmation_text, parse_mode='HTML', reply_markup=reply_markup)
  1357. except ValueError:
  1358. await update.message.reply_text("โŒ Invalid price format. Please use numbers only.")
  1359. except Exception as e:
  1360. await update.message.reply_text(f"โŒ Error processing stop loss command: {e}")
  1361. async def tp_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  1362. """Handle the /tp (take profit) command for setting take profit orders."""
  1363. if not self.is_authorized(update.effective_chat.id):
  1364. await update.message.reply_text("โŒ Unauthorized access.")
  1365. return
  1366. try:
  1367. if not context.args or len(context.args) < 2:
  1368. await update.message.reply_text(
  1369. "โŒ Usage: /tp [token] [price]\n"
  1370. "Example: /tp BTC 50000\n\n"
  1371. "This creates a take profit order at the specified price."
  1372. )
  1373. return
  1374. token = context.args[0].upper()
  1375. profit_price = float(context.args[1])
  1376. symbol = f"{token}/USDC:USDC"
  1377. # Get current positions to find the position for this token
  1378. positions = self.client.get_positions()
  1379. if positions is None:
  1380. await update.message.reply_text(f"โŒ Could not fetch positions to check {token} position")
  1381. return
  1382. # Find the position for this token
  1383. current_position = None
  1384. for position in positions:
  1385. if position.get('symbol') == symbol and float(position.get('contracts', 0)) != 0:
  1386. current_position = position
  1387. break
  1388. if not current_position:
  1389. await update.message.reply_text(f"๐Ÿ“ญ No open position found for {token}\n\nYou need an open position to set a take profit.")
  1390. return
  1391. # Extract position details
  1392. contracts = float(current_position.get('contracts', 0))
  1393. entry_price = float(current_position.get('entryPx', 0))
  1394. unrealized_pnl = float(current_position.get('unrealizedPnl', 0))
  1395. # Determine position direction and validate take profit price
  1396. if contracts > 0:
  1397. # Long position - take profit should be above entry price
  1398. position_type = "LONG"
  1399. exit_side = "sell"
  1400. exit_emoji = "๐Ÿ”ด"
  1401. contracts_abs = contracts
  1402. if profit_price <= entry_price:
  1403. await update.message.reply_text(
  1404. f"โš ๏ธ Take profit price should be ABOVE entry price for long positions\n\n"
  1405. f"๐Ÿ“Š Your {token} LONG position:\n"
  1406. f"โ€ข Entry Price: ${entry_price:,.2f}\n"
  1407. f"โ€ข Take Profit: ${profit_price:,.2f} โŒ\n\n"
  1408. f"๐Ÿ’ก Try a higher price like: /tp {token} {entry_price * 1.05:.0f}"
  1409. )
  1410. return
  1411. else:
  1412. # Short position - take profit should be below entry price
  1413. position_type = "SHORT"
  1414. exit_side = "buy"
  1415. exit_emoji = "๐ŸŸข"
  1416. contracts_abs = abs(contracts)
  1417. if profit_price >= entry_price:
  1418. await update.message.reply_text(
  1419. f"โš ๏ธ Take profit price should be BELOW entry price for short positions\n\n"
  1420. f"๐Ÿ“Š Your {token} SHORT position:\n"
  1421. f"โ€ข Entry Price: ${entry_price:,.2f}\n"
  1422. f"โ€ข Take Profit: ${profit_price:,.2f} โŒ\n\n"
  1423. f"๐Ÿ’ก Try a lower price like: /tp {token} {entry_price * 0.95:.0f}"
  1424. )
  1425. return
  1426. # Get current market price for reference
  1427. market_data = self.client.get_market_data(symbol)
  1428. current_price = 0
  1429. if market_data:
  1430. current_price = float(market_data['ticker'].get('last', 0))
  1431. # Calculate estimated P&L at take profit
  1432. if contracts > 0: # Long position
  1433. pnl_at_tp = (profit_price - entry_price) * contracts_abs
  1434. else: # Short position
  1435. pnl_at_tp = (entry_price - profit_price) * contracts_abs
  1436. # Create confirmation message
  1437. pnl_emoji = "๐ŸŸข" if pnl_at_tp >= 0 else "๐Ÿ”ด"
  1438. confirmation_text = f"""
  1439. ๐ŸŽฏ <b>Take Profit Order Confirmation</b>
  1440. ๐Ÿ“Š <b>Position Details:</b>
  1441. โ€ข Token: {token}
  1442. โ€ข Position: {position_type}
  1443. โ€ข Size: {contracts_abs} contracts
  1444. โ€ข Entry Price: ${entry_price:,.2f}
  1445. โ€ข Current Price: ${current_price:,.2f}
  1446. ๐Ÿ’ฐ <b>Take Profit Order:</b>
  1447. โ€ข Target Price: ${profit_price:,.2f}
  1448. โ€ข Action: {exit_side.upper()} (Close {position_type})
  1449. โ€ข Amount: {contracts_abs} {token}
  1450. โ€ข Order Type: Limit Order
  1451. โ€ข {pnl_emoji} Est. P&L: ${pnl_at_tp:,.2f}
  1452. โš ๏ธ <b>Are you sure you want to set this take profit?</b>
  1453. This will place a limit {exit_side} order at ${profit_price:,.2f} to capture profits from your {position_type} position.
  1454. """
  1455. keyboard = [
  1456. [
  1457. InlineKeyboardButton(f"โœ… Set Take Profit", callback_data=f"confirm_tp_{token}_{exit_side}_{contracts_abs}_{profit_price}"),
  1458. InlineKeyboardButton("โŒ Cancel", callback_data="cancel_order")
  1459. ]
  1460. ]
  1461. reply_markup = InlineKeyboardMarkup(keyboard)
  1462. await update.message.reply_text(confirmation_text, parse_mode='HTML', reply_markup=reply_markup)
  1463. except ValueError:
  1464. await update.message.reply_text("โŒ Invalid price format. Please use numbers only.")
  1465. except Exception as e:
  1466. await update.message.reply_text(f"โŒ Error processing take profit command: {e}")
  1467. async def start_order_monitoring(self):
  1468. """Start the order monitoring background task."""
  1469. if self.monitoring_active:
  1470. return
  1471. self.monitoring_active = True
  1472. logger.info("๐Ÿ”„ Starting order monitoring...")
  1473. # Initialize tracking data
  1474. await self._initialize_order_tracking()
  1475. # Start monitoring loop
  1476. asyncio.create_task(self._order_monitoring_loop())
  1477. async def stop_order_monitoring(self):
  1478. """Stop the order monitoring background task."""
  1479. self.monitoring_active = False
  1480. logger.info("โน๏ธ Stopping order monitoring...")
  1481. async def _initialize_order_tracking(self):
  1482. """Initialize order and position tracking."""
  1483. try:
  1484. # Get current open orders to initialize tracking
  1485. orders = self.client.get_open_orders()
  1486. if orders:
  1487. self.last_known_orders = {order.get('id') for order in orders if order.get('id')}
  1488. logger.info(f"๐Ÿ“‹ Initialized tracking with {len(self.last_known_orders)} open orders")
  1489. # Get current positions for P&L tracking
  1490. positions = self.client.get_positions()
  1491. if positions:
  1492. for position in positions:
  1493. symbol = position.get('symbol')
  1494. contracts = float(position.get('contracts', 0))
  1495. entry_price = float(position.get('entryPx', 0))
  1496. if symbol and contracts != 0:
  1497. self.last_known_positions[symbol] = {
  1498. 'contracts': contracts,
  1499. 'entry_price': entry_price
  1500. }
  1501. logger.info(f"๐Ÿ“Š Initialized tracking with {len(self.last_known_positions)} positions")
  1502. except Exception as e:
  1503. logger.error(f"โŒ Error initializing order tracking: {e}")
  1504. async def _order_monitoring_loop(self):
  1505. """Main monitoring loop that runs every Config.BOT_HEARTBEAT_SECONDS seconds."""
  1506. while self.monitoring_active:
  1507. try:
  1508. await self._check_order_fills()
  1509. await asyncio.sleep(Config.BOT_HEARTBEAT_SECONDS) # Use configurable interval
  1510. except asyncio.CancelledError:
  1511. logger.info("๐Ÿ›‘ Order monitoring cancelled")
  1512. break
  1513. except Exception as e:
  1514. logger.error(f"โŒ Error in order monitoring loop: {e}")
  1515. await asyncio.sleep(Config.BOT_HEARTBEAT_SECONDS) # Continue monitoring even if there's an error
  1516. async def _check_order_fills(self):
  1517. """Check for filled orders and send notifications."""
  1518. try:
  1519. # Get current orders and positions
  1520. current_orders = self.client.get_open_orders() or []
  1521. current_positions = self.client.get_positions() or []
  1522. # Get current order IDs
  1523. current_order_ids = {order.get('id') for order in current_orders if order.get('id')}
  1524. # Find filled orders (orders that were in last_known_orders but not in current_orders)
  1525. filled_order_ids = self.last_known_orders - current_order_ids
  1526. if filled_order_ids:
  1527. logger.info(f"๐ŸŽฏ Detected {len(filled_order_ids)} filled orders")
  1528. await self._process_filled_orders(filled_order_ids, current_positions)
  1529. # Update tracking data
  1530. self.last_known_orders = current_order_ids
  1531. await self._update_position_tracking(current_positions)
  1532. # Check price alarms
  1533. await self._check_price_alarms()
  1534. # Check external trades (trades made outside the bot)
  1535. await self._check_external_trades()
  1536. # Check stop losses (if risk management is enabled)
  1537. if Config.RISK_MANAGEMENT_ENABLED:
  1538. await self._check_stop_losses(current_positions)
  1539. except Exception as e:
  1540. logger.error(f"โŒ Error checking order fills: {e}")
  1541. async def _check_price_alarms(self):
  1542. """Check all active price alarms."""
  1543. try:
  1544. # Get all active alarms
  1545. active_alarms = self.alarm_manager.get_all_active_alarms()
  1546. if not active_alarms:
  1547. return
  1548. # Get unique tokens from alarms
  1549. tokens_to_check = list(set(alarm['token'] for alarm in active_alarms))
  1550. # Fetch current prices for all tokens
  1551. price_data = {}
  1552. for token in tokens_to_check:
  1553. symbol = f"{token}/USDC:USDC"
  1554. market_data = self.client.get_market_data(symbol)
  1555. if market_data and market_data.get('ticker'):
  1556. current_price = market_data['ticker'].get('last')
  1557. if current_price is not None:
  1558. price_data[token] = float(current_price)
  1559. # Check alarms against current prices
  1560. triggered_alarms = self.alarm_manager.check_alarms(price_data)
  1561. # Send notifications for triggered alarms
  1562. for alarm in triggered_alarms:
  1563. await self._send_alarm_notification(alarm)
  1564. except Exception as e:
  1565. logger.error(f"โŒ Error checking price alarms: {e}")
  1566. async def _send_alarm_notification(self, alarm: Dict[str, Any]):
  1567. """Send notification for triggered alarm."""
  1568. try:
  1569. message = self.alarm_manager.format_triggered_alarm(alarm)
  1570. await self.send_message(message)
  1571. logger.info(f"๐Ÿ“ข Sent alarm notification: {alarm['token']} ID {alarm['id']} @ ${alarm['triggered_price']}")
  1572. except Exception as e:
  1573. logger.error(f"โŒ Error sending alarm notification: {e}")
  1574. async def _check_external_trades(self):
  1575. """Check for trades made outside the Telegram bot and update stats."""
  1576. try:
  1577. # Get recent fills from Hyperliquid
  1578. recent_fills = self.client.get_recent_fills()
  1579. if not recent_fills:
  1580. return
  1581. # Initialize last processed time if first run
  1582. if self.last_processed_trade_time is None:
  1583. # Set to current time minus 1 hour to catch recent activity
  1584. self.last_processed_trade_time = (datetime.now() - timedelta(hours=1)).isoformat()
  1585. # Filter for new trades since last check
  1586. new_trades = []
  1587. latest_trade_time = self.last_processed_trade_time
  1588. for fill in recent_fills:
  1589. fill_time = fill.get('timestamp')
  1590. if fill_time:
  1591. # Convert timestamps to comparable format
  1592. try:
  1593. # Convert fill_time to string if it's not already
  1594. if isinstance(fill_time, (int, float)):
  1595. # Assume it's a unix timestamp
  1596. fill_time_str = datetime.fromtimestamp(fill_time / 1000 if fill_time > 1e10 else fill_time).isoformat()
  1597. else:
  1598. fill_time_str = str(fill_time)
  1599. # Compare as strings
  1600. if fill_time_str > self.last_processed_trade_time:
  1601. new_trades.append(fill)
  1602. if fill_time_str > latest_trade_time:
  1603. latest_trade_time = fill_time_str
  1604. except Exception as timestamp_error:
  1605. logger.warning(f"โš ๏ธ Error processing timestamp {fill_time}: {timestamp_error}")
  1606. continue
  1607. if not new_trades:
  1608. return
  1609. # Process new trades
  1610. for trade in new_trades:
  1611. await self._process_external_trade(trade)
  1612. # Update last processed time
  1613. self.last_processed_trade_time = latest_trade_time
  1614. if new_trades:
  1615. logger.info(f"๐Ÿ“Š Processed {len(new_trades)} external trades")
  1616. except Exception as e:
  1617. logger.error(f"โŒ Error checking external trades: {e}")
  1618. async def _process_external_trade(self, trade: Dict[str, Any]):
  1619. """Process an individual external trade."""
  1620. try:
  1621. # Extract trade information
  1622. symbol = trade.get('symbol', '')
  1623. side = trade.get('side', '')
  1624. amount = float(trade.get('amount', 0))
  1625. price = float(trade.get('price', 0))
  1626. trade_id = trade.get('id', 'external')
  1627. timestamp = trade.get('timestamp', '')
  1628. if not all([symbol, side, amount, price]):
  1629. return
  1630. # Record trade in stats
  1631. self.stats.record_trade(symbol, side, amount, price, trade_id)
  1632. # Send notification for significant trades
  1633. await self._send_external_trade_notification(trade)
  1634. logger.info(f"๐Ÿ“‹ Processed external trade: {side} {amount} {symbol} @ ${price}")
  1635. except Exception as e:
  1636. logger.error(f"โŒ Error processing external trade: {e}")
  1637. async def _send_external_trade_notification(self, trade: Dict[str, Any]):
  1638. """Send notification for external trades."""
  1639. try:
  1640. symbol = trade.get('symbol', '')
  1641. side = trade.get('side', '')
  1642. amount = float(trade.get('amount', 0))
  1643. price = float(trade.get('price', 0))
  1644. timestamp = trade.get('timestamp', '')
  1645. # Extract token from symbol
  1646. token = symbol.split('/')[0] if '/' in symbol else symbol
  1647. # Format timestamp
  1648. try:
  1649. trade_time = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
  1650. time_str = trade_time.strftime('%H:%M:%S')
  1651. except:
  1652. time_str = "Unknown"
  1653. # Determine trade type and emoji
  1654. side_emoji = "๐ŸŸข" if side.lower() == 'buy' else "๐Ÿ”ด"
  1655. trade_value = amount * price
  1656. message = f"""
  1657. ๐Ÿ”„ <b>External Trade Detected</b>
  1658. ๐Ÿ“Š <b>Trade Details:</b>
  1659. โ€ข Token: {token}
  1660. โ€ข Side: {side.upper()}
  1661. โ€ข Amount: {amount} {token}
  1662. โ€ข Price: ${price:,.2f}
  1663. โ€ข Value: ${trade_value:,.2f}
  1664. {side_emoji} <b>Source:</b> Direct Platform Trade
  1665. โฐ <b>Time:</b> {time_str}
  1666. ๐Ÿ“ˆ <b>Note:</b> This trade was executed outside the Telegram bot
  1667. ๐Ÿ“Š Stats have been automatically updated
  1668. """
  1669. await self.send_message(message.strip())
  1670. logger.info(f"๐Ÿ“ข Sent external trade notification: {side} {amount} {token}")
  1671. except Exception as e:
  1672. logger.error(f"โŒ Error sending external trade notification: {e}")
  1673. async def _check_stop_losses(self, current_positions: list):
  1674. """Check all positions for stop loss triggers and execute automatic exits."""
  1675. try:
  1676. if not current_positions:
  1677. return
  1678. stop_loss_triggers = []
  1679. for position in current_positions:
  1680. symbol = position.get('symbol')
  1681. contracts = float(position.get('contracts', 0))
  1682. entry_price = float(position.get('entryPx', 0))
  1683. if not symbol or contracts == 0 or entry_price == 0:
  1684. continue
  1685. # Get current market price
  1686. market_data = self.client.get_market_data(symbol)
  1687. if not market_data or not market_data.get('ticker'):
  1688. continue
  1689. current_price = float(market_data['ticker'].get('last', 0))
  1690. if current_price == 0:
  1691. continue
  1692. # Calculate current P&L percentage
  1693. if contracts > 0: # Long position
  1694. pnl_percent = ((current_price - entry_price) / entry_price) * 100
  1695. else: # Short position
  1696. pnl_percent = ((entry_price - current_price) / entry_price) * 100
  1697. # Check if stop loss should trigger
  1698. if pnl_percent <= -Config.STOP_LOSS_PERCENTAGE:
  1699. token = symbol.split('/')[0] if '/' in symbol else symbol
  1700. stop_loss_triggers.append({
  1701. 'symbol': symbol,
  1702. 'token': token,
  1703. 'contracts': contracts,
  1704. 'entry_price': entry_price,
  1705. 'current_price': current_price,
  1706. 'pnl_percent': pnl_percent
  1707. })
  1708. # Execute stop losses
  1709. for trigger in stop_loss_triggers:
  1710. await self._execute_automatic_stop_loss(trigger)
  1711. except Exception as e:
  1712. logger.error(f"โŒ Error checking stop losses: {e}")
  1713. async def _execute_automatic_stop_loss(self, trigger: Dict[str, Any]):
  1714. """Execute an automatic stop loss order."""
  1715. try:
  1716. symbol = trigger['symbol']
  1717. token = trigger['token']
  1718. contracts = trigger['contracts']
  1719. entry_price = trigger['entry_price']
  1720. current_price = trigger['current_price']
  1721. pnl_percent = trigger['pnl_percent']
  1722. # Determine the exit side (opposite of position)
  1723. exit_side = 'sell' if contracts > 0 else 'buy'
  1724. contracts_abs = abs(contracts)
  1725. # Send notification before executing
  1726. await self._send_stop_loss_notification(trigger, "triggered")
  1727. # Execute the stop loss order (market order for immediate execution)
  1728. try:
  1729. if exit_side == 'sell':
  1730. order = self.client.create_market_sell_order(symbol, contracts_abs)
  1731. else:
  1732. order = self.client.create_market_buy_order(symbol, contracts_abs)
  1733. if order:
  1734. logger.info(f"๐Ÿ›‘ Stop loss executed: {token} {exit_side} {contracts_abs} @ ${current_price}")
  1735. # Record the trade in stats
  1736. self.stats.record_trade(symbol, exit_side, contracts_abs, current_price, order.get('id', 'stop_loss'))
  1737. # Send success notification
  1738. await self._send_stop_loss_notification(trigger, "executed", order)
  1739. else:
  1740. logger.error(f"โŒ Stop loss order failed for {token}")
  1741. await self._send_stop_loss_notification(trigger, "failed")
  1742. except Exception as order_error:
  1743. logger.error(f"โŒ Stop loss order execution failed for {token}: {order_error}")
  1744. await self._send_stop_loss_notification(trigger, "failed", error=str(order_error))
  1745. except Exception as e:
  1746. logger.error(f"โŒ Error executing automatic stop loss: {e}")
  1747. async def _send_stop_loss_notification(self, trigger: Dict[str, Any], status: str, order: Dict = None, error: str = None):
  1748. """Send notification for stop loss events."""
  1749. try:
  1750. token = trigger['token']
  1751. contracts = trigger['contracts']
  1752. entry_price = trigger['entry_price']
  1753. current_price = trigger['current_price']
  1754. pnl_percent = trigger['pnl_percent']
  1755. position_type = "LONG" if contracts > 0 else "SHORT"
  1756. contracts_abs = abs(contracts)
  1757. if status == "triggered":
  1758. title = "๐Ÿ›‘ Stop Loss Triggered"
  1759. status_text = f"Stop loss triggered at {Config.STOP_LOSS_PERCENTAGE}% loss"
  1760. emoji = "๐Ÿšจ"
  1761. elif status == "executed":
  1762. title = "โœ… Stop Loss Executed"
  1763. status_text = "Position closed automatically"
  1764. emoji = "๐Ÿ›‘"
  1765. elif status == "failed":
  1766. title = "โŒ Stop Loss Failed"
  1767. status_text = f"Stop loss execution failed{': ' + error if error else ''}"
  1768. emoji = "โš ๏ธ"
  1769. else:
  1770. return
  1771. # Calculate loss
  1772. loss_value = contracts_abs * abs(current_price - entry_price)
  1773. message = f"""
  1774. {title}
  1775. {emoji} <b>Risk Management Alert</b>
  1776. ๐Ÿ“Š <b>Position Details:</b>
  1777. โ€ข Token: {token}
  1778. โ€ข Direction: {position_type}
  1779. โ€ข Size: {contracts_abs} contracts
  1780. โ€ข Entry Price: ${entry_price:,.2f}
  1781. โ€ข Current Price: ${current_price:,.2f}
  1782. ๐Ÿ”ด <b>Loss Details:</b>
  1783. โ€ข Loss: ${loss_value:,.2f} ({pnl_percent:.2f}%)
  1784. โ€ข Stop Loss Threshold: {Config.STOP_LOSS_PERCENTAGE}%
  1785. ๐Ÿ“‹ <b>Action:</b> {status_text}
  1786. โฐ <b>Time:</b> {datetime.now().strftime('%H:%M:%S')}
  1787. """
  1788. if order and status == "executed":
  1789. order_id = order.get('id', 'N/A')
  1790. message += f"\n๐Ÿ†” <b>Order ID:</b> {order_id}"
  1791. await self.send_message(message.strip())
  1792. logger.info(f"๐Ÿ“ข Sent stop loss notification: {token} {status}")
  1793. except Exception as e:
  1794. logger.error(f"โŒ Error sending stop loss notification: {e}")
  1795. async def _process_filled_orders(self, filled_order_ids: set, current_positions: list):
  1796. """Process filled orders and determine if they opened or closed positions."""
  1797. try:
  1798. # Create a map of current positions
  1799. current_position_map = {}
  1800. for position in current_positions:
  1801. symbol = position.get('symbol')
  1802. contracts = float(position.get('contracts', 0))
  1803. if symbol:
  1804. current_position_map[symbol] = contracts
  1805. # For each symbol, check if position size changed
  1806. for symbol, old_position_data in self.last_known_positions.items():
  1807. old_contracts = old_position_data['contracts']
  1808. current_contracts = current_position_map.get(symbol, 0)
  1809. if old_contracts != current_contracts:
  1810. # Position changed - determine if it's open or close
  1811. await self._handle_position_change(symbol, old_position_data, current_contracts)
  1812. # Check for new positions (symbols not in last_known_positions)
  1813. for symbol, current_contracts in current_position_map.items():
  1814. if symbol not in self.last_known_positions and current_contracts != 0:
  1815. # New position opened
  1816. await self._handle_new_position(symbol, current_contracts)
  1817. except Exception as e:
  1818. logger.error(f"โŒ Error processing filled orders: {e}")
  1819. async def _handle_position_change(self, symbol: str, old_position_data: dict, current_contracts: float):
  1820. """Handle when an existing position changes size."""
  1821. old_contracts = old_position_data['contracts']
  1822. old_entry_price = old_position_data['entry_price']
  1823. # Get current market price
  1824. market_data = self.client.get_market_data(symbol)
  1825. current_price = 0
  1826. if market_data:
  1827. current_price = float(market_data['ticker'].get('last', 0))
  1828. token = symbol.split('/')[0] if '/' in symbol else symbol
  1829. if current_contracts == 0 and old_contracts != 0:
  1830. # Position closed
  1831. await self._send_close_trade_notification(token, old_contracts, old_entry_price, current_price)
  1832. elif abs(current_contracts) > abs(old_contracts):
  1833. # Position increased
  1834. added_contracts = current_contracts - old_contracts
  1835. await self._send_open_trade_notification(token, added_contracts, current_price, "increased")
  1836. elif abs(current_contracts) < abs(old_contracts):
  1837. # Position decreased (partial close)
  1838. closed_contracts = old_contracts - current_contracts
  1839. await self._send_partial_close_notification(token, closed_contracts, old_entry_price, current_price)
  1840. async def _handle_new_position(self, symbol: str, contracts: float):
  1841. """Handle when a new position is opened."""
  1842. # Get current market price
  1843. market_data = self.client.get_market_data(symbol)
  1844. current_price = 0
  1845. if market_data:
  1846. current_price = float(market_data['ticker'].get('last', 0))
  1847. token = symbol.split('/')[0] if '/' in symbol else symbol
  1848. await self._send_open_trade_notification(token, contracts, current_price, "opened")
  1849. async def _update_position_tracking(self, current_positions: list):
  1850. """Update the position tracking data."""
  1851. new_position_map = {}
  1852. for position in current_positions:
  1853. symbol = position.get('symbol')
  1854. contracts = float(position.get('contracts', 0))
  1855. entry_price = float(position.get('entryPx', 0))
  1856. if symbol and contracts != 0:
  1857. new_position_map[symbol] = {
  1858. 'contracts': contracts,
  1859. 'entry_price': entry_price
  1860. }
  1861. self.last_known_positions = new_position_map
  1862. async def _send_open_trade_notification(self, token: str, contracts: float, price: float, action: str):
  1863. """Send notification for opened/increased position."""
  1864. position_type = "LONG" if contracts > 0 else "SHORT"
  1865. contracts_abs = abs(contracts)
  1866. value = contracts_abs * price
  1867. if action == "opened":
  1868. title = "๐Ÿš€ Position Opened"
  1869. action_text = f"New {position_type} position opened"
  1870. else:
  1871. title = "๐Ÿ“ˆ Position Increased"
  1872. action_text = f"{position_type} position increased"
  1873. message = f"""
  1874. {title}
  1875. ๐Ÿ“Š <b>Trade Details:</b>
  1876. โ€ข Token: {token}
  1877. โ€ข Direction: {position_type}
  1878. โ€ข Size: {contracts_abs} contracts
  1879. โ€ข Entry Price: ${price:,.2f}
  1880. โ€ข Value: ${value:,.2f}
  1881. โœ… <b>Status:</b> {action_text}
  1882. โฐ <b>Time:</b> {datetime.now().strftime('%H:%M:%S')}
  1883. ๐Ÿ“ฑ Use /positions to view all positions
  1884. """
  1885. await self.send_message(message.strip())
  1886. logger.info(f"๐Ÿ“ข Sent open trade notification: {token} {position_type} {contracts_abs} @ ${price}")
  1887. async def _send_close_trade_notification(self, token: str, contracts: float, entry_price: float, exit_price: float):
  1888. """Send notification for closed position with P&L."""
  1889. position_type = "LONG" if contracts > 0 else "SHORT"
  1890. contracts_abs = abs(contracts)
  1891. # Calculate P&L
  1892. if contracts > 0: # Long position
  1893. pnl = (exit_price - entry_price) * contracts_abs
  1894. else: # Short position
  1895. pnl = (entry_price - exit_price) * contracts_abs
  1896. pnl_percent = (pnl / (entry_price * contracts_abs)) * 100 if entry_price > 0 else 0
  1897. pnl_emoji = "๐ŸŸข" if pnl >= 0 else "๐Ÿ”ด"
  1898. exit_value = contracts_abs * exit_price
  1899. message = f"""
  1900. ๐ŸŽฏ <b>Position Closed</b>
  1901. ๐Ÿ“Š <b>Trade Summary:</b>
  1902. โ€ข Token: {token}
  1903. โ€ข Direction: {position_type}
  1904. โ€ข Size: {contracts_abs} contracts
  1905. โ€ข Entry Price: ${entry_price:,.2f}
  1906. โ€ข Exit Price: ${exit_price:,.2f}
  1907. โ€ข Exit Value: ${exit_value:,.2f}
  1908. {pnl_emoji} <b>Profit & Loss:</b>
  1909. โ€ข P&L: ${pnl:,.2f} ({pnl_percent:+.2f}%)
  1910. โ€ข Result: {"PROFIT" if pnl >= 0 else "LOSS"}
  1911. โœ… <b>Status:</b> Position fully closed
  1912. โฐ <b>Time:</b> {datetime.now().strftime('%H:%M:%S')}
  1913. ๐Ÿ“Š Use /stats to view updated performance
  1914. """
  1915. await self.send_message(message.strip())
  1916. logger.info(f"๐Ÿ“ข Sent close trade notification: {token} {position_type} P&L: ${pnl:.2f}")
  1917. async def _send_partial_close_notification(self, token: str, contracts: float, entry_price: float, exit_price: float):
  1918. """Send notification for partially closed position."""
  1919. position_type = "LONG" if contracts > 0 else "SHORT"
  1920. contracts_abs = abs(contracts)
  1921. # Calculate P&L for closed portion
  1922. if contracts > 0: # Long position
  1923. pnl = (exit_price - entry_price) * contracts_abs
  1924. else: # Short position
  1925. pnl = (entry_price - exit_price) * contracts_abs
  1926. pnl_percent = (pnl / (entry_price * contracts_abs)) * 100 if entry_price > 0 else 0
  1927. pnl_emoji = "๐ŸŸข" if pnl >= 0 else "๐Ÿ”ด"
  1928. message = f"""
  1929. ๐Ÿ“‰ <b>Position Partially Closed</b>
  1930. ๐Ÿ“Š <b>Partial Close Details:</b>
  1931. โ€ข Token: {token}
  1932. โ€ข Direction: {position_type}
  1933. โ€ข Closed Size: {contracts_abs} contracts
  1934. โ€ข Entry Price: ${entry_price:,.2f}
  1935. โ€ข Exit Price: ${exit_price:,.2f}
  1936. {pnl_emoji} <b>Partial P&L:</b>
  1937. โ€ข P&L: ${pnl:,.2f} ({pnl_percent:+.2f}%)
  1938. โœ… <b>Status:</b> Partial position closed
  1939. โฐ <b>Time:</b> {datetime.now().strftime('%H:%M:%S')}
  1940. ๐Ÿ“ˆ Use /positions to view remaining position
  1941. """
  1942. await self.send_message(message.strip())
  1943. logger.info(f"๐Ÿ“ข Sent partial close notification: {token} {position_type} Partial P&L: ${pnl:.2f}")
  1944. async def monitoring_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  1945. """Handle the /monitoring command to show monitoring status."""
  1946. if not self.is_authorized(update.effective_chat.id):
  1947. await update.message.reply_text("โŒ Unauthorized access.")
  1948. return
  1949. # Get alarm statistics
  1950. alarm_stats = self.alarm_manager.get_statistics()
  1951. status_text = f"""
  1952. ๐Ÿ”„ <b>System Monitoring Status</b>
  1953. ๐Ÿ“Š <b>Order Monitoring:</b>
  1954. โ€ข Active: {'โœ… Yes' if self.monitoring_active else 'โŒ No'}
  1955. โ€ข Check Interval: {Config.BOT_HEARTBEAT_SECONDS} seconds
  1956. โ€ข Orders Tracked: {len(self.last_known_orders)}
  1957. โ€ข Positions Tracked: {len(self.last_known_positions)}
  1958. ๐Ÿ”” <b>Price Alarms:</b>
  1959. โ€ข Active Alarms: {alarm_stats['total_active']}
  1960. โ€ข Triggered Today: {alarm_stats['total_triggered']}
  1961. โ€ข Tokens Monitored: {alarm_stats['tokens_tracked']}
  1962. โ€ข Next Alarm ID: {alarm_stats['next_id']}
  1963. ๐Ÿ”„ <b>External Trade Monitoring:</b>
  1964. โ€ข Last Check: {self.last_processed_trade_time or 'Not started'}
  1965. โ€ข Auto Stats Update: โœ… Enabled
  1966. โ€ข External Notifications: โœ… Enabled
  1967. ๐Ÿ›ก๏ธ <b>Risk Management:</b>
  1968. โ€ข Automatic Stop Loss: {'โœ… Enabled' if Config.RISK_MANAGEMENT_ENABLED else 'โŒ Disabled'}
  1969. โ€ข Stop Loss Threshold: {Config.STOP_LOSS_PERCENTAGE}%
  1970. โ€ข Position Monitoring: {'โœ… Active' if Config.RISK_MANAGEMENT_ENABLED else 'โŒ Inactive'}
  1971. ๐Ÿ“ˆ <b>Notifications:</b>
  1972. โ€ข ๐Ÿš€ Position Opened/Increased
  1973. โ€ข ๐Ÿ“‰ Position Partially/Fully Closed
  1974. โ€ข ๐ŸŽฏ P&L Calculations
  1975. โ€ข ๐Ÿ”” Price Alarm Triggers
  1976. โ€ข ๐Ÿ”„ External Trade Detection
  1977. โ€ข ๐Ÿ›‘ Automatic Stop Loss Triggers
  1978. โฐ <b>Last Check:</b> {datetime.now().strftime('%H:%M:%S')}
  1979. ๐Ÿ’ก <b>Monitoring Features:</b>
  1980. โ€ข Real-time order fill detection
  1981. โ€ข Automatic P&L calculation
  1982. โ€ข Position change tracking
  1983. โ€ข Price alarm monitoring
  1984. โ€ข External trade monitoring
  1985. โ€ข Auto stats synchronization
  1986. โ€ข Instant Telegram notifications
  1987. """
  1988. if alarm_stats['token_breakdown']:
  1989. status_text += f"\n\n๐Ÿ“‹ <b>Active Alarms by Token:</b>\n"
  1990. for token, count in alarm_stats['token_breakdown'].items():
  1991. status_text += f"โ€ข {token}: {count} alarm{'s' if count != 1 else ''}\n"
  1992. await update.message.reply_text(status_text.strip(), parse_mode='HTML')
  1993. async def alarm_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  1994. """Handle the /alarm command for price alerts."""
  1995. if not self.is_authorized(update.effective_chat.id):
  1996. await update.message.reply_text("โŒ Unauthorized access.")
  1997. return
  1998. try:
  1999. if not context.args or len(context.args) == 0:
  2000. # No arguments - list all alarms
  2001. alarms = self.alarm_manager.get_all_active_alarms()
  2002. message = self.alarm_manager.format_alarm_list(alarms)
  2003. await update.message.reply_text(message, parse_mode='HTML')
  2004. return
  2005. elif len(context.args) == 1:
  2006. arg = context.args[0]
  2007. # Check if argument is a number (alarm ID to remove)
  2008. try:
  2009. alarm_id = int(arg)
  2010. # Remove alarm by ID
  2011. if self.alarm_manager.remove_alarm(alarm_id):
  2012. await update.message.reply_text(f"โœ… Alarm ID {alarm_id} has been removed.")
  2013. else:
  2014. await update.message.reply_text(f"โŒ Alarm ID {alarm_id} not found.")
  2015. return
  2016. except ValueError:
  2017. # Not a number, treat as token
  2018. token = arg.upper()
  2019. alarms = self.alarm_manager.get_alarms_by_token(token)
  2020. message = self.alarm_manager.format_alarm_list(alarms, f"{token} Price Alarms")
  2021. await update.message.reply_text(message, parse_mode='HTML')
  2022. return
  2023. elif len(context.args) == 2:
  2024. # Set new alarm: /alarm TOKEN PRICE
  2025. token = context.args[0].upper()
  2026. target_price = float(context.args[1])
  2027. # Get current market price
  2028. symbol = f"{token}/USDC:USDC"
  2029. market_data = self.client.get_market_data(symbol)
  2030. if not market_data or not market_data.get('ticker'):
  2031. await update.message.reply_text(f"โŒ Could not fetch current price for {token}")
  2032. return
  2033. current_price = float(market_data['ticker'].get('last', 0))
  2034. if current_price <= 0:
  2035. await update.message.reply_text(f"โŒ Invalid current price for {token}")
  2036. return
  2037. # Create the alarm
  2038. alarm = self.alarm_manager.create_alarm(token, target_price, current_price)
  2039. # Format confirmation message
  2040. direction_emoji = "๐Ÿ“ˆ" if alarm['direction'] == 'above' else "๐Ÿ“‰"
  2041. price_diff = abs(target_price - current_price)
  2042. price_diff_percent = (price_diff / current_price) * 100
  2043. message = f"""
  2044. โœ… <b>Price Alarm Created</b>
  2045. ๐Ÿ“Š <b>Alarm Details:</b>
  2046. โ€ข Alarm ID: {alarm['id']}
  2047. โ€ข Token: {token}
  2048. โ€ข Target Price: ${target_price:,.2f}
  2049. โ€ข Current Price: ${current_price:,.2f}
  2050. โ€ข Direction: {alarm['direction'].upper()}
  2051. {direction_emoji} <b>Alert Condition:</b>
  2052. Will trigger when {token} price moves {alarm['direction']} ${target_price:,.2f}
  2053. ๐Ÿ’ฐ <b>Price Difference:</b>
  2054. โ€ข Distance: ${price_diff:,.2f} ({price_diff_percent:.2f}%)
  2055. โ€ข Status: ACTIVE โœ…
  2056. โฐ <b>Created:</b> {datetime.now().strftime('%H:%M:%S')}
  2057. ๐Ÿ’ก The alarm will be checked every {Config.BOT_HEARTBEAT_SECONDS} seconds and you'll receive a notification when triggered.
  2058. """
  2059. await update.message.reply_text(message.strip(), parse_mode='HTML')
  2060. else:
  2061. # Too many arguments
  2062. await update.message.reply_text(
  2063. "โŒ Invalid usage. Examples:\n\n"
  2064. "โ€ข <code>/alarm</code> - List all alarms\n"
  2065. "โ€ข <code>/alarm BTC</code> - List BTC alarms\n"
  2066. "โ€ข <code>/alarm BTC 50000</code> - Set alarm for BTC at $50,000\n"
  2067. "โ€ข <code>/alarm 3</code> - Remove alarm ID 3",
  2068. parse_mode='HTML'
  2069. )
  2070. except ValueError:
  2071. await update.message.reply_text("โŒ Invalid price format. Please use numbers only.")
  2072. except Exception as e:
  2073. error_message = f"โŒ Error processing alarm command: {str(e)}"
  2074. await update.message.reply_text(error_message)
  2075. logger.error(f"Error in alarm command: {e}")
  2076. async def logs_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  2077. """Handle the /logs command to show log file statistics and cleanup options."""
  2078. if not self.is_authorized(update.effective_chat.id):
  2079. await update.message.reply_text("โŒ Unauthorized access.")
  2080. return
  2081. try:
  2082. # Check for cleanup argument
  2083. if context.args and len(context.args) >= 1:
  2084. if context.args[0].lower() == 'cleanup':
  2085. # Get days parameter (default 30)
  2086. days_to_keep = 30
  2087. if len(context.args) >= 2:
  2088. try:
  2089. days_to_keep = int(context.args[1])
  2090. except ValueError:
  2091. await update.message.reply_text("โŒ Invalid number of days. Using default (30).")
  2092. # Perform cleanup
  2093. await update.message.reply_text(f"๐Ÿงน Cleaning up log files older than {days_to_keep} days...")
  2094. cleanup_logs(days_to_keep)
  2095. await update.message.reply_text(f"โœ… Log cleanup completed!")
  2096. return
  2097. # Show log statistics
  2098. log_stats_text = format_log_stats()
  2099. # Add additional info
  2100. status_text = f"""
  2101. ๐Ÿ“Š <b>System Logging Status</b>
  2102. {log_stats_text}
  2103. ๐Ÿ“ˆ <b>Log Configuration:</b>
  2104. โ€ข Log Level: {Config.LOG_LEVEL}
  2105. โ€ข Heartbeat Interval: {Config.BOT_HEARTBEAT_SECONDS}s
  2106. โ€ข Bot Uptime: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
  2107. ๐Ÿ’ก <b>Log Management:</b>
  2108. โ€ข <code>/logs cleanup</code> - Clean old logs (30 days)
  2109. โ€ข <code>/logs cleanup 7</code> - Clean logs older than 7 days
  2110. โ€ข Log rotation happens automatically
  2111. โ€ข Old backups are removed automatically
  2112. ๐Ÿ”ง <b>Configuration:</b>
  2113. โ€ข Rotation Type: {Config.LOG_ROTATION_TYPE}
  2114. โ€ข Max Size: {Config.LOG_MAX_SIZE_MB}MB (size rotation)
  2115. โ€ข Backup Count: {Config.LOG_BACKUP_COUNT}
  2116. """
  2117. await update.message.reply_text(status_text.strip(), parse_mode='HTML')
  2118. except Exception as e:
  2119. error_message = f"โŒ Error processing logs command: {str(e)}"
  2120. await update.message.reply_text(error_message)
  2121. logger.error(f"Error in logs command: {e}")
  2122. async def performance_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  2123. """Handle the /performance command to show token performance ranking or detailed stats."""
  2124. if not self.is_authorized(update.effective_chat.id):
  2125. await update.message.reply_text("โŒ Unauthorized access.")
  2126. return
  2127. try:
  2128. # Check if specific token is requested
  2129. if context.args and len(context.args) >= 1:
  2130. # Detailed performance for specific token
  2131. token = context.args[0].upper()
  2132. await self._show_token_performance(update, token)
  2133. else:
  2134. # Show token performance ranking
  2135. await self._show_performance_ranking(update)
  2136. except Exception as e:
  2137. error_message = f"โŒ Error processing performance command: {str(e)}"
  2138. await update.message.reply_text(error_message)
  2139. logger.error(f"Error in performance command: {e}")
  2140. async def _show_performance_ranking(self, update: Update):
  2141. """Show token performance ranking (compressed view)."""
  2142. token_performance = self.stats.get_token_performance()
  2143. if not token_performance:
  2144. await update.message.reply_text(
  2145. "๐Ÿ“Š <b>Token Performance</b>\n\n"
  2146. "๐Ÿ“ญ No trading data available yet.\n\n"
  2147. "๐Ÿ’ก Performance tracking starts after your first completed trades.\n"
  2148. "Use /long or /short to start trading!",
  2149. parse_mode='HTML'
  2150. )
  2151. return
  2152. # Sort tokens by total P&L (best to worst)
  2153. sorted_tokens = sorted(
  2154. token_performance.items(),
  2155. key=lambda x: x[1]['total_pnl'],
  2156. reverse=True
  2157. )
  2158. performance_text = "๐Ÿ† <b>Token Performance Ranking</b>\n\n"
  2159. # Add ranking with emojis
  2160. for i, (token, stats) in enumerate(sorted_tokens, 1):
  2161. # Ranking emoji
  2162. if i == 1:
  2163. rank_emoji = "๐Ÿฅ‡"
  2164. elif i == 2:
  2165. rank_emoji = "๐Ÿฅˆ"
  2166. elif i == 3:
  2167. rank_emoji = "๐Ÿฅ‰"
  2168. else:
  2169. rank_emoji = f"#{i}"
  2170. # P&L emoji
  2171. pnl_emoji = "๐ŸŸข" if stats['total_pnl'] >= 0 else "๐Ÿ”ด"
  2172. # Format the line
  2173. performance_text += f"{rank_emoji} <b>{token}</b>\n"
  2174. performance_text += f" {pnl_emoji} P&L: ${stats['total_pnl']:,.2f} ({stats['pnl_percentage']:+.1f}%)\n"
  2175. performance_text += f" ๐Ÿ“Š Trades: {stats['completed_trades']}"
  2176. # Add win rate if there are completed trades
  2177. if stats['completed_trades'] > 0:
  2178. performance_text += f" | Win: {stats['win_rate']:.0f}%"
  2179. performance_text += "\n\n"
  2180. # Add summary
  2181. total_pnl = sum(stats['total_pnl'] for stats in token_performance.values())
  2182. total_trades = sum(stats['completed_trades'] for stats in token_performance.values())
  2183. total_pnl_emoji = "๐ŸŸข" if total_pnl >= 0 else "๐Ÿ”ด"
  2184. performance_text += f"๐Ÿ’ผ <b>Portfolio Summary:</b>\n"
  2185. performance_text += f" {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
  2186. performance_text += f" ๐Ÿ“ˆ Tokens Traded: {len(token_performance)}\n"
  2187. performance_text += f" ๐Ÿ”„ Completed Trades: {total_trades}\n\n"
  2188. performance_text += f"๐Ÿ’ก <b>Usage:</b> <code>/performance BTC</code> for detailed {Config.DEFAULT_TRADING_TOKEN} stats"
  2189. await update.message.reply_text(performance_text.strip(), parse_mode='HTML')
  2190. async def _show_token_performance(self, update: Update, token: str):
  2191. """Show detailed performance for a specific token."""
  2192. token_stats = self.stats.get_token_detailed_stats(token)
  2193. # Check if token has any data
  2194. if token_stats.get('total_trades', 0) == 0:
  2195. await update.message.reply_text(
  2196. f"๐Ÿ“Š <b>{token} Performance</b>\n\n"
  2197. f"๐Ÿ“ญ No trading history found for {token}.\n\n"
  2198. f"๐Ÿ’ก Start trading {token} with:\n"
  2199. f"โ€ข <code>/long {token} 100</code>\n"
  2200. f"โ€ข <code>/short {token} 100</code>\n\n"
  2201. f"๐Ÿ”„ Use <code>/performance</code> to see all token rankings.",
  2202. parse_mode='HTML'
  2203. )
  2204. return
  2205. # Check if there's a message (no completed trades)
  2206. if 'message' in token_stats and token_stats.get('completed_trades', 0) == 0:
  2207. await update.message.reply_text(
  2208. f"๐Ÿ“Š <b>{token} Performance</b>\n\n"
  2209. f"{token_stats['message']}\n\n"
  2210. f"๐Ÿ“ˆ <b>Current Activity:</b>\n"
  2211. f"โ€ข Total Trades: {token_stats['total_trades']}\n"
  2212. f"โ€ข Buy Orders: {token_stats.get('buy_trades', 0)}\n"
  2213. f"โ€ข Sell Orders: {token_stats.get('sell_trades', 0)}\n"
  2214. f"โ€ข Volume: ${token_stats.get('total_volume', 0):,.2f}\n\n"
  2215. f"๐Ÿ’ก Complete some trades to see P&L statistics!\n"
  2216. f"๐Ÿ”„ Use <code>/performance</code> to see all token rankings.",
  2217. parse_mode='HTML'
  2218. )
  2219. return
  2220. # Detailed stats display
  2221. pnl_emoji = "๐ŸŸข" if token_stats['total_pnl'] >= 0 else "๐Ÿ”ด"
  2222. performance_text = f"""
  2223. ๐Ÿ“Š <b>{token} Detailed Performance</b>
  2224. ๐Ÿ’ฐ <b>P&L Summary:</b>
  2225. โ€ข {pnl_emoji} Total P&L: ${token_stats['total_pnl']:,.2f} ({token_stats['pnl_percentage']:+.2f}%)
  2226. โ€ข ๐Ÿ’ต Total Volume: ${token_stats['completed_volume']:,.2f}
  2227. โ€ข ๐Ÿ“ˆ Expectancy: ${token_stats['expectancy']:,.2f}
  2228. ๐Ÿ“Š <b>Trading Activity:</b>
  2229. โ€ข Total Trades: {token_stats['total_trades']}
  2230. โ€ข Completed: {token_stats['completed_trades']}
  2231. โ€ข Buy Orders: {token_stats['buy_trades']}
  2232. โ€ข Sell Orders: {token_stats['sell_trades']}
  2233. ๐Ÿ† <b>Performance Metrics:</b>
  2234. โ€ข Win Rate: {token_stats['win_rate']:.1f}%
  2235. โ€ข Profit Factor: {token_stats['profit_factor']:.2f}
  2236. โ€ข Wins: {token_stats['total_wins']} | Losses: {token_stats['total_losses']}
  2237. ๐Ÿ’ก <b>Best/Worst:</b>
  2238. โ€ข Largest Win: ${token_stats['largest_win']:,.2f}
  2239. โ€ข Largest Loss: ${token_stats['largest_loss']:,.2f}
  2240. โ€ข Avg Win: ${token_stats['avg_win']:,.2f}
  2241. โ€ข Avg Loss: ${token_stats['avg_loss']:,.2f}
  2242. """
  2243. # Add recent trades if available
  2244. if token_stats.get('recent_trades'):
  2245. performance_text += f"\n๐Ÿ”„ <b>Recent Trades:</b>\n"
  2246. for trade in token_stats['recent_trades'][-3:]: # Last 3 trades
  2247. trade_time = datetime.fromisoformat(trade['timestamp']).strftime('%m/%d %H:%M')
  2248. side_emoji = "๐ŸŸข" if trade['side'] == 'buy' else "๐Ÿ”ด"
  2249. pnl_display = f" | P&L: ${trade.get('pnl', 0):.2f}" if trade.get('pnl', 0) != 0 else ""
  2250. performance_text += f"โ€ข {side_emoji} {trade['side'].upper()} ${trade['value']:,.0f} @ {trade_time}{pnl_display}\n"
  2251. performance_text += f"\n๐Ÿ”„ Use <code>/performance</code> to see all token rankings"
  2252. await update.message.reply_text(performance_text.strip(), parse_mode='HTML')
  2253. async def daily_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  2254. """Handle the /daily command to show daily performance stats."""
  2255. if not self.is_authorized(update.effective_chat.id):
  2256. await update.message.reply_text("โŒ Unauthorized access.")
  2257. return
  2258. try:
  2259. daily_stats = self.stats.get_daily_stats(10)
  2260. if not daily_stats:
  2261. await update.message.reply_text(
  2262. "๐Ÿ“… <b>Daily Performance</b>\n\n"
  2263. "๐Ÿ“ญ No daily performance data available yet.\n\n"
  2264. "๐Ÿ’ก Daily stats are calculated from completed trades.\n"
  2265. "Start trading to see daily performance!",
  2266. parse_mode='HTML'
  2267. )
  2268. return
  2269. daily_text = "๐Ÿ“… <b>Daily Performance (Last 10 Days)</b>\n\n"
  2270. total_pnl = 0
  2271. total_trades = 0
  2272. for day_stats in daily_stats:
  2273. pnl_emoji = "๐ŸŸข" if day_stats['pnl'] >= 0 else "๐Ÿ”ด"
  2274. daily_text += f"๐Ÿ“Š <b>{day_stats['date_formatted']}</b>\n"
  2275. daily_text += f" {pnl_emoji} P&L: ${day_stats['pnl']:,.2f} ({day_stats['pnl_pct']:+.1f}%)\n"
  2276. daily_text += f" ๐Ÿ”„ Trades: {day_stats['trades']}\n\n"
  2277. total_pnl += day_stats['pnl']
  2278. total_trades += day_stats['trades']
  2279. # Add summary
  2280. total_pnl_emoji = "๐ŸŸข" if total_pnl >= 0 else "๐Ÿ”ด"
  2281. daily_text += f"๐Ÿ’ผ <b>10-Day Summary:</b>\n"
  2282. daily_text += f" {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
  2283. daily_text += f" ๐Ÿ”„ Total Trades: {total_trades}\n"
  2284. daily_text += f" ๐Ÿ“Š Avg per Day: ${total_pnl/len(daily_stats):,.2f}"
  2285. await update.message.reply_text(daily_text.strip(), parse_mode='HTML')
  2286. except Exception as e:
  2287. error_message = f"โŒ Error processing daily command: {str(e)}"
  2288. await update.message.reply_text(error_message)
  2289. logger.error(f"Error in daily command: {e}")
  2290. async def weekly_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  2291. """Handle the /weekly command to show weekly performance stats."""
  2292. if not self.is_authorized(update.effective_chat.id):
  2293. await update.message.reply_text("โŒ Unauthorized access.")
  2294. return
  2295. try:
  2296. weekly_stats = self.stats.get_weekly_stats(10)
  2297. if not weekly_stats:
  2298. await update.message.reply_text(
  2299. "๐Ÿ“Š <b>Weekly Performance</b>\n\n"
  2300. "๐Ÿ“ญ No weekly performance data available yet.\n\n"
  2301. "๐Ÿ’ก Weekly stats are calculated from completed trades.\n"
  2302. "Start trading to see weekly performance!",
  2303. parse_mode='HTML'
  2304. )
  2305. return
  2306. weekly_text = "๐Ÿ“Š <b>Weekly Performance (Last 10 Weeks)</b>\n\n"
  2307. total_pnl = 0
  2308. total_trades = 0
  2309. for week_stats in weekly_stats:
  2310. pnl_emoji = "๐ŸŸข" if week_stats['pnl'] >= 0 else "๐Ÿ”ด"
  2311. weekly_text += f"๐Ÿ“ˆ <b>{week_stats['week_formatted']}</b>\n"
  2312. weekly_text += f" {pnl_emoji} P&L: ${week_stats['pnl']:,.2f} ({week_stats['pnl_pct']:+.1f}%)\n"
  2313. weekly_text += f" ๐Ÿ”„ Trades: {week_stats['trades']}\n\n"
  2314. total_pnl += week_stats['pnl']
  2315. total_trades += week_stats['trades']
  2316. # Add summary
  2317. total_pnl_emoji = "๐ŸŸข" if total_pnl >= 0 else "๐Ÿ”ด"
  2318. weekly_text += f"๐Ÿ’ผ <b>10-Week Summary:</b>\n"
  2319. weekly_text += f" {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
  2320. weekly_text += f" ๐Ÿ”„ Total Trades: {total_trades}\n"
  2321. weekly_text += f" ๐Ÿ“Š Avg per Week: ${total_pnl/len(weekly_stats):,.2f}"
  2322. await update.message.reply_text(weekly_text.strip(), parse_mode='HTML')
  2323. except Exception as e:
  2324. error_message = f"โŒ Error processing weekly command: {str(e)}"
  2325. await update.message.reply_text(error_message)
  2326. logger.error(f"Error in weekly command: {e}")
  2327. async def monthly_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  2328. """Handle the /monthly command to show monthly performance stats."""
  2329. if not self.is_authorized(update.effective_chat.id):
  2330. await update.message.reply_text("โŒ Unauthorized access.")
  2331. return
  2332. try:
  2333. monthly_stats = self.stats.get_monthly_stats(10)
  2334. if not monthly_stats:
  2335. await update.message.reply_text(
  2336. "๐Ÿ“† <b>Monthly Performance</b>\n\n"
  2337. "๐Ÿ“ญ No monthly performance data available yet.\n\n"
  2338. "๐Ÿ’ก Monthly stats are calculated from completed trades.\n"
  2339. "Start trading to see monthly performance!",
  2340. parse_mode='HTML'
  2341. )
  2342. return
  2343. monthly_text = "๐Ÿ“† <b>Monthly Performance (Last 10 Months)</b>\n\n"
  2344. total_pnl = 0
  2345. total_trades = 0
  2346. for month_stats in monthly_stats:
  2347. pnl_emoji = "๐ŸŸข" if month_stats['pnl'] >= 0 else "๐Ÿ”ด"
  2348. monthly_text += f"๐Ÿ“… <b>{month_stats['month_formatted']}</b>\n"
  2349. monthly_text += f" {pnl_emoji} P&L: ${month_stats['pnl']:,.2f} ({month_stats['pnl_pct']:+.1f}%)\n"
  2350. monthly_text += f" ๐Ÿ”„ Trades: {month_stats['trades']}\n\n"
  2351. total_pnl += month_stats['pnl']
  2352. total_trades += month_stats['trades']
  2353. # Add summary
  2354. total_pnl_emoji = "๐ŸŸข" if total_pnl >= 0 else "๐Ÿ”ด"
  2355. monthly_text += f"๐Ÿ’ผ <b>10-Month Summary:</b>\n"
  2356. monthly_text += f" {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
  2357. monthly_text += f" ๐Ÿ”„ Total Trades: {total_trades}\n"
  2358. monthly_text += f" ๐Ÿ“Š Avg per Month: ${total_pnl/len(monthly_stats):,.2f}"
  2359. await update.message.reply_text(monthly_text.strip(), parse_mode='HTML')
  2360. except Exception as e:
  2361. error_message = f"โŒ Error processing monthly command: {str(e)}"
  2362. await update.message.reply_text(error_message)
  2363. logger.error(f"Error in monthly command: {e}")
  2364. async def main_async():
  2365. """Async main entry point for the Telegram bot."""
  2366. try:
  2367. # Validate configuration
  2368. if not Config.validate():
  2369. logger.error("โŒ Configuration validation failed!")
  2370. return
  2371. if not Config.TELEGRAM_ENABLED:
  2372. logger.error("โŒ Telegram is not enabled in configuration")
  2373. return
  2374. # Create and run the bot
  2375. bot = TelegramTradingBot()
  2376. await bot.run()
  2377. except KeyboardInterrupt:
  2378. logger.info("๐Ÿ‘‹ Bot stopped by user")
  2379. except Exception as e:
  2380. logger.error(f"โŒ Unexpected error: {e}")
  2381. raise
  2382. def main():
  2383. """Main entry point for the Telegram bot."""
  2384. try:
  2385. # Check if we're already in an asyncio context
  2386. try:
  2387. loop = asyncio.get_running_loop()
  2388. # If we get here, we're already in an asyncio context
  2389. logger.error("โŒ Cannot run main() from within an asyncio context. Use main_async() instead.")
  2390. return
  2391. except RuntimeError:
  2392. # No running loop, safe to use asyncio.run()
  2393. pass
  2394. # Run the async main function
  2395. asyncio.run(main_async())
  2396. except Exception as e:
  2397. logger.error(f"โŒ Failed to start telegram bot: {e}")
  2398. raise
  2399. if __name__ == "__main__":
  2400. main()