telegram_bot.py 149 KB

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