telegram_bot.py 112 KB

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