info_commands.py 73 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481
  1. #!/usr/bin/env python3
  2. """
  3. Info Commands - Handles information-related Telegram commands.
  4. """
  5. import logging
  6. from datetime import datetime
  7. from typing import Optional, Dict, Any, List
  8. from telegram import Update
  9. from telegram.ext import ContextTypes
  10. from src.config.config import Config
  11. from src.utils.price_formatter import format_price_with_symbol, get_formatter
  12. logger = logging.getLogger(__name__)
  13. class InfoCommands:
  14. """Handles all information-related Telegram commands."""
  15. def __init__(self, trading_engine, notification_manager=None):
  16. """Initialize with trading engine and notification manager."""
  17. self.trading_engine = trading_engine
  18. self.notification_manager = notification_manager
  19. def _is_authorized(self, chat_id: str) -> bool:
  20. """Check if the chat ID is authorized."""
  21. return str(chat_id) == str(Config.TELEGRAM_CHAT_ID)
  22. async def balance_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  23. """Handle the /balance command."""
  24. chat_id = update.effective_chat.id
  25. if not self._is_authorized(chat_id):
  26. await context.bot.send_message(chat_id=chat_id, text="โŒ Unauthorized access.")
  27. return
  28. balance = self.trading_engine.get_balance()
  29. if balance:
  30. balance_text = "๐Ÿ’ฐ <b>Account Balance</b>\n\n"
  31. # Debug: Show raw balance structure (can be removed after debugging)
  32. logger.debug(f"Raw balance data: {balance}")
  33. # CCXT balance structure includes 'free', 'used', and 'total'
  34. total_balance = balance.get('total', {})
  35. free_balance = balance.get('free', {})
  36. used_balance = balance.get('used', {})
  37. # Get total portfolio value
  38. total_portfolio_value = 0
  39. # Show USDC balance prominently
  40. if 'USDC' in total_balance:
  41. usdc_total = float(total_balance['USDC'])
  42. usdc_free = float(free_balance.get('USDC', 0))
  43. usdc_used = float(used_balance.get('USDC', 0))
  44. balance_text += f"๐Ÿ’ต <b>USDC:</b>\n"
  45. balance_text += f" ๐Ÿ“Š Total: ${usdc_total:,.2f}\n"
  46. balance_text += f" โœ… Available: ${usdc_free:,.2f}\n"
  47. balance_text += f" ๐Ÿ”’ In Use: ${usdc_used:,.2f}\n\n"
  48. total_portfolio_value += usdc_total
  49. # Show other non-zero balances
  50. other_assets = []
  51. for asset, amount in total_balance.items():
  52. if asset != 'USDC' and float(amount) > 0:
  53. other_assets.append((asset, float(amount)))
  54. if other_assets:
  55. balance_text += "๐Ÿ“Š <b>Other Assets:</b>\n"
  56. for asset, amount in other_assets:
  57. free_amount = float(free_balance.get(asset, 0))
  58. used_amount = float(used_balance.get(asset, 0))
  59. balance_text += f"๐Ÿ’ต <b>{asset}:</b>\n"
  60. balance_text += f" ๐Ÿ“Š Total: {amount:.6f}\n"
  61. balance_text += f" โœ… Available: {free_amount:.6f}\n"
  62. balance_text += f" ๐Ÿ”’ In Use: {used_amount:.6f}\n\n"
  63. # Portfolio summary
  64. usdc_balance = float(total_balance.get('USDC', 0))
  65. stats = self.trading_engine.get_stats()
  66. if stats:
  67. basic_stats = stats.get_basic_stats()
  68. initial_balance = basic_stats.get('initial_balance', usdc_balance)
  69. pnl = usdc_balance - initial_balance
  70. pnl_percent = (pnl / initial_balance * 100) if initial_balance > 0 else 0
  71. pnl_emoji = "๐ŸŸข" if pnl >= 0 else "๐Ÿ”ด"
  72. balance_text += f"๐Ÿ’ผ <b>Portfolio Summary:</b>\n"
  73. balance_text += f" ๐Ÿ’ฐ Total Value: ${total_portfolio_value:,.2f}\n"
  74. balance_text += f" ๐Ÿš€ Available for Trading: ${float(free_balance.get('USDC', 0)):,.2f}\n"
  75. balance_text += f" ๐Ÿ”’ In Active Use: ${float(used_balance.get('USDC', 0)):,.2f}\n\n"
  76. balance_text += f"๐Ÿ“Š <b>Performance:</b>\n"
  77. balance_text += f" ๐Ÿ’ต Initial: ${initial_balance:,.2f}\n"
  78. balance_text += f" {pnl_emoji} P&L: ${pnl:,.2f} ({pnl_percent:+.2f}%)\n"
  79. await context.bot.send_message(chat_id=chat_id, text=balance_text, parse_mode='HTML')
  80. else:
  81. await context.bot.send_message(chat_id=chat_id, text="โŒ Could not fetch balance information")
  82. async def positions_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  83. """Handle the /positions command."""
  84. chat_id = update.effective_chat.id
  85. if not self._is_authorized(chat_id):
  86. await context.bot.send_message(chat_id=chat_id, text="โŒ Unauthorized access.")
  87. return
  88. # ๐Ÿงน PHASE 4: Use unified trades table as the single source of truth
  89. stats = self.trading_engine.get_stats()
  90. if not stats:
  91. await context.bot.send_message(chat_id=chat_id, text="โŒ Trading statistics not available.")
  92. return
  93. # ๐Ÿ†• AUTO-SYNC: Check for positions on exchange that don't have trade lifecycle records
  94. # Use cached data from MarketMonitor if available (updated every heartbeat)
  95. if (hasattr(self.trading_engine, 'market_monitor') and
  96. self.trading_engine.market_monitor and
  97. hasattr(self.trading_engine.market_monitor, 'get_cached_positions')):
  98. cache_age = self.trading_engine.market_monitor.get_cache_age_seconds()
  99. if cache_age < 60: # Use cached data if less than 1 minute old
  100. exchange_positions = self.trading_engine.market_monitor.get_cached_positions() or []
  101. logger.debug(f"Using cached positions for auto-sync (age: {cache_age:.1f}s)")
  102. else:
  103. exchange_positions = self.trading_engine.get_positions() or []
  104. logger.debug("Using fresh API call for auto-sync (cache too old)")
  105. else:
  106. exchange_positions = self.trading_engine.get_positions() or []
  107. logger.debug("Using fresh API call for auto-sync (no cache available)")
  108. synced_positions = []
  109. for exchange_pos in exchange_positions:
  110. symbol = exchange_pos.get('symbol')
  111. contracts = float(exchange_pos.get('contracts', 0))
  112. if symbol and abs(contracts) > 0:
  113. # Check if we have a trade lifecycle record for this position
  114. existing_trade = stats.get_trade_by_symbol_and_status(symbol, 'position_opened')
  115. if not existing_trade:
  116. # ๐Ÿšจ ORPHANED POSITION: Auto-create trade lifecycle record using exchange data
  117. entry_price = float(exchange_pos.get('entryPrice', 0))
  118. position_side = 'long' if contracts > 0 else 'short'
  119. order_side = 'buy' if contracts > 0 else 'sell'
  120. # โœ… Use exchange data - no need to estimate!
  121. if entry_price > 0:
  122. logger.info(f"๐Ÿ”„ Auto-syncing orphaned position: {symbol} {position_side} {abs(contracts)} @ ${entry_price} (exchange data)")
  123. else:
  124. # Fallback only if exchange truly doesn't provide entry price
  125. entry_price = await self._estimate_entry_price_for_orphaned_position(symbol, contracts)
  126. logger.warning(f"๐Ÿ”„ Auto-syncing orphaned position: {symbol} {position_side} {abs(contracts)} @ ${entry_price} (estimated)")
  127. # Create trade lifecycle for external position
  128. lifecycle_id = stats.create_trade_lifecycle(
  129. symbol=symbol,
  130. side=order_side,
  131. entry_order_id=f"external_sync_{int(datetime.now().timestamp())}",
  132. trade_type='external'
  133. )
  134. if lifecycle_id:
  135. # Update to position_opened status
  136. success = stats.update_trade_position_opened(
  137. lifecycle_id=lifecycle_id,
  138. entry_price=entry_price,
  139. entry_amount=abs(contracts),
  140. exchange_fill_id=f"external_fill_{int(datetime.now().timestamp())}"
  141. )
  142. if success:
  143. synced_positions.append(symbol)
  144. logger.info(f"โœ… Successfully synced orphaned position for {symbol}")
  145. # ๐Ÿ†• Send immediate notification for auto-synced position
  146. token = symbol.split('/')[0] if '/' in symbol else symbol
  147. unrealized_pnl = float(exchange_pos.get('unrealizedPnl', 0))
  148. position_value = float(exchange_pos.get('notional', 0))
  149. liquidation_price = float(exchange_pos.get('liquidationPrice', 0))
  150. leverage = float(exchange_pos.get('leverage', 1))
  151. pnl_percentage = float(exchange_pos.get('percentage', 0))
  152. pnl_emoji = "๐ŸŸข" if unrealized_pnl >= 0 else "๐Ÿ”ด"
  153. notification_text = (
  154. f"๐Ÿ”„ <b>Position Auto-Synced</b>\n\n"
  155. f"๐ŸŽฏ Token: {token}\n"
  156. f"๐Ÿ“ˆ Direction: {position_side.upper()}\n"
  157. f"๐Ÿ“ Size: {abs(contracts):.6f} {token}\n"
  158. f"๐Ÿ’ฐ Entry: ${entry_price:,.4f}\n"
  159. f"๐Ÿ’ต Value: ${position_value:,.2f}\n"
  160. f"{pnl_emoji} P&L: ${unrealized_pnl:,.2f} ({pnl_percentage:+.2f}%)\n"
  161. )
  162. if leverage > 1:
  163. notification_text += f"โšก Leverage: {leverage:.1f}x\n"
  164. if liquidation_price > 0:
  165. notification_text += f"โš ๏ธ Liquidation: ${liquidation_price:,.2f}\n"
  166. notification_text += (
  167. f"\n๐Ÿ“ Reason: Position opened outside bot\n"
  168. f"โฐ Time: {datetime.now().strftime('%H:%M:%S')}\n\n"
  169. f"โœ… Position now tracked in bot\n"
  170. f"๐Ÿ’ก Use /sl {token} [price] to set stop loss"
  171. )
  172. # Send notification via trading engine's notification manager
  173. if self.notification_manager:
  174. try:
  175. await self.notification_manager.send_generic_notification(notification_text)
  176. logger.info(f"๐Ÿ“ค Sent auto-sync notification for {symbol}")
  177. except Exception as e:
  178. logger.error(f"โŒ Failed to send auto-sync notification: {e}")
  179. else:
  180. logger.warning(f"โš ๏ธ No notification manager available for auto-sync notification")
  181. else:
  182. logger.error(f"โŒ Failed to sync orphaned position for {symbol}")
  183. else:
  184. logger.error(f"โŒ Failed to create lifecycle for orphaned position {symbol}")
  185. if synced_positions:
  186. sync_msg = f"๐Ÿ”„ <b>Auto-synced {len(synced_positions)} orphaned position(s):</b> {', '.join([s.split('/')[0] for s in synced_positions])}\n\n"
  187. else:
  188. sync_msg = ""
  189. # Get open positions from unified trades table (now including any newly synced ones)
  190. open_positions = stats.get_open_positions()
  191. positions_text = f"๐Ÿ“ˆ <b>Open Positions</b>\n\n{sync_msg}"
  192. if open_positions:
  193. total_unrealized = 0
  194. total_position_value = 0
  195. # Also get fresh exchange data for display
  196. fresh_exchange_positions = self.trading_engine.get_positions() or []
  197. exchange_data_map = {pos.get('symbol'): pos for pos in fresh_exchange_positions}
  198. for position_trade in open_positions:
  199. symbol = position_trade['symbol']
  200. token = symbol.split('/')[0] if '/' in symbol else symbol
  201. position_side = position_trade['position_side'] # 'long' or 'short'
  202. entry_price = position_trade['entry_price']
  203. current_amount = position_trade['current_position_size']
  204. trade_type = position_trade.get('trade_type', 'manual')
  205. # ๐Ÿ†• Use fresh exchange data if available (most accurate)
  206. exchange_pos = exchange_data_map.get(symbol)
  207. if exchange_pos:
  208. # Use exchange's official data
  209. unrealized_pnl = float(exchange_pos.get('unrealizedPnl', 0))
  210. mark_price = float(exchange_pos.get('markPrice') or 0)
  211. position_value = float(exchange_pos.get('notional', 0))
  212. liquidation_price = float(exchange_pos.get('liquidationPrice', 0))
  213. margin_used = float(exchange_pos.get('initialMargin', 0))
  214. leverage = float(exchange_pos.get('leverage', 1))
  215. pnl_percentage = float(exchange_pos.get('percentage', 0))
  216. # Get mark price from market data if not in position data
  217. if mark_price <= 0:
  218. try:
  219. market_data = self.trading_engine.get_market_data(symbol)
  220. if market_data and market_data.get('ticker'):
  221. mark_price = float(market_data['ticker'].get('last', entry_price))
  222. except:
  223. mark_price = entry_price # Fallback
  224. else:
  225. # Fallback to our calculation if exchange data unavailable
  226. unrealized_pnl = position_trade.get('unrealized_pnl', 0)
  227. mark_price = entry_price # Fallback
  228. try:
  229. market_data = self.trading_engine.get_market_data(symbol)
  230. if market_data and market_data.get('ticker'):
  231. mark_price = float(market_data['ticker'].get('last', entry_price))
  232. # Calculate unrealized PnL with current price
  233. if position_side == 'long':
  234. unrealized_pnl = current_amount * (mark_price - entry_price)
  235. else: # Short position
  236. unrealized_pnl = current_amount * (entry_price - mark_price)
  237. except:
  238. pass # Use entry price as fallback
  239. position_value = abs(current_amount) * mark_price
  240. liquidation_price = None
  241. margin_used = None
  242. leverage = None
  243. pnl_percentage = (unrealized_pnl / position_value * 100) if position_value > 0 else 0
  244. total_position_value += position_value
  245. total_unrealized += unrealized_pnl
  246. # Position emoji and formatting
  247. if position_side == 'long':
  248. pos_emoji = "๐ŸŸข"
  249. direction = "LONG"
  250. else: # Short position
  251. pos_emoji = "๐Ÿ”ด"
  252. direction = "SHORT"
  253. pnl_emoji = "๐ŸŸข" if unrealized_pnl >= 0 else "๐Ÿ”ด"
  254. # Format prices with proper precision for this token
  255. formatter = get_formatter()
  256. entry_price_str = formatter.format_price_with_symbol(entry_price, token)
  257. mark_price_str = formatter.format_price_with_symbol(mark_price, token)
  258. # Trade type indicator
  259. type_indicator = ""
  260. if trade_type == 'external':
  261. type_indicator = " ๐Ÿ”„" # External/synced position
  262. elif trade_type == 'bot':
  263. type_indicator = " ๐Ÿค–" # Bot-created position
  264. positions_text += f"{pos_emoji} <b>{token} ({direction}){type_indicator}</b>\n"
  265. positions_text += f" ๐Ÿ“ Size: {abs(current_amount):.6f} {token}\n"
  266. positions_text += f" ๐Ÿ’ฐ Entry: {entry_price_str}\n"
  267. positions_text += f" ๐Ÿ“Š Mark: {mark_price_str}\n"
  268. positions_text += f" ๐Ÿ’ต Value: ${position_value:,.2f}\n"
  269. positions_text += f" {pnl_emoji} P&L: ${unrealized_pnl:,.2f} ({pnl_percentage:+.2f}%)\n"
  270. # Show exchange-provided risk data if available
  271. if leverage:
  272. positions_text += f" โšก Leverage: {leverage:.1f}x\n"
  273. if margin_used:
  274. positions_text += f" ๐Ÿ’ณ Margin: ${margin_used:,.2f}\n"
  275. if liquidation_price:
  276. liq_price_str = formatter.format_price_with_symbol(liquidation_price, token)
  277. positions_text += f" โš ๏ธ Liquidation: {liq_price_str}\n"
  278. # Show stop loss if linked
  279. if position_trade.get('stop_loss_price'):
  280. sl_price = position_trade['stop_loss_price']
  281. sl_status = "Pending" if not position_trade.get('stop_loss_order_id') else "Active"
  282. positions_text += f" ๐Ÿ›‘ Stop Loss: {formatter.format_price_with_symbol(sl_price, token)} ({sl_status})\n"
  283. # Show take profit if linked
  284. if position_trade.get('take_profit_price'):
  285. tp_price = position_trade['take_profit_price']
  286. tp_status = "Pending" if not position_trade.get('take_profit_order_id') else "Active"
  287. positions_text += f" ๐ŸŽฏ Take Profit: {formatter.format_price_with_symbol(tp_price, token)} ({tp_status})\n"
  288. positions_text += f" ๐Ÿ†” Lifecycle ID: {position_trade['trade_lifecycle_id'][:8]}\n\n"
  289. # Portfolio summary
  290. portfolio_emoji = "๐ŸŸข" if total_unrealized >= 0 else "๐Ÿ”ด"
  291. positions_text += f"๐Ÿ’ผ <b>Total Portfolio:</b>\n"
  292. positions_text += f" ๐Ÿ’ต Total Value: ${total_position_value:,.2f}\n"
  293. positions_text += f" {portfolio_emoji} Total P&L: ${total_unrealized:,.2f}\n\n"
  294. positions_text += f"๐Ÿค– <b>Legend:</b> ๐Ÿค– Bot-created โ€ข ๐Ÿ”„ External/synced\n"
  295. positions_text += f"๐Ÿ’ก Use /sl [token] [price] or /tp [token] [price] to set risk management"
  296. else:
  297. positions_text += "๐Ÿ“ญ No open positions\n\n"
  298. positions_text += "๐Ÿ’ก Use /long or /short to open a position"
  299. await context.bot.send_message(chat_id=chat_id, text=positions_text, parse_mode='HTML')
  300. async def orders_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  301. """Handle the /orders command."""
  302. chat_id = update.effective_chat.id
  303. if not self._is_authorized(chat_id):
  304. await context.bot.send_message(chat_id=chat_id, text="โŒ Unauthorized access.")
  305. return
  306. orders = self.trading_engine.get_orders()
  307. if orders is not None:
  308. if len(orders) > 0:
  309. orders_text = "๐Ÿ“‹ <b>Open Orders</b>\n\n"
  310. # Group orders by symbol
  311. orders_by_symbol = {}
  312. for order in orders:
  313. symbol = order.get('symbol', '').replace('/USDC:USDC', '')
  314. if symbol not in orders_by_symbol:
  315. orders_by_symbol[symbol] = []
  316. orders_by_symbol[symbol].append(order)
  317. for symbol, symbol_orders in orders_by_symbol.items():
  318. orders_text += f"๐Ÿ“Š <b>{symbol}</b>\n"
  319. formatter = get_formatter()
  320. for order in symbol_orders:
  321. side = order.get('side', '').upper()
  322. amount = float(order.get('amount', 0))
  323. price = float(order.get('price', 0))
  324. order_type = order.get('type', 'unknown').title()
  325. order_id = order.get('id', 'N/A')
  326. # Order emoji
  327. side_emoji = "๐ŸŸข" if side == "BUY" else "๐Ÿ”ด"
  328. orders_text += f" {side_emoji} {side} {amount:.6f} @ {formatter.format_price_with_symbol(price, symbol)}\n"
  329. orders_text += f" ๐Ÿ“‹ Type: {order_type} | ID: {order_id}\n"
  330. # Check for pending stop losses linked to this order
  331. stats = self.trading_engine.get_stats()
  332. if stats:
  333. # Try to find this order in our database to get its bot_order_ref_id
  334. order_in_db = stats.get_order_by_exchange_id(order_id)
  335. if order_in_db:
  336. bot_ref_id = order_in_db.get('bot_order_ref_id')
  337. if bot_ref_id:
  338. # Look for pending stop losses with this order as parent
  339. pending_sls = stats.get_orders_by_status(
  340. status='pending_trigger',
  341. order_type_filter='stop_limit_trigger',
  342. parent_bot_order_ref_id=bot_ref_id
  343. )
  344. if pending_sls:
  345. sl_order = pending_sls[0] # Should only be one
  346. sl_price = sl_order.get('price', 0)
  347. sl_side = sl_order.get('side', '').upper()
  348. orders_text += f" ๐Ÿ›‘ Pending SL: {sl_side} @ {formatter.format_price_with_symbol(sl_price, symbol)} (activates when filled)\n"
  349. orders_text += "\n"
  350. orders_text += f"๐Ÿ’ผ <b>Total Orders:</b> {len(orders)}\n"
  351. orders_text += f"๐Ÿ’ก Use /coo [token] to cancel orders"
  352. else:
  353. orders_text = "๐Ÿ“‹ <b>Open Orders</b>\n\n"
  354. orders_text += "๐Ÿ“ญ No open orders\n\n"
  355. orders_text += "๐Ÿ’ก Use /long, /short, /sl, or /tp to create orders"
  356. await context.bot.send_message(chat_id=chat_id, text=orders_text, parse_mode='HTML')
  357. else:
  358. await context.bot.send_message(chat_id=chat_id, text="โŒ Could not fetch orders")
  359. async def stats_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  360. """Handle the /stats command."""
  361. chat_id = update.effective_chat.id
  362. if not self._is_authorized(chat_id):
  363. await context.bot.send_message(chat_id=chat_id, text="โŒ Unauthorized access.")
  364. return
  365. # Get current balance for stats
  366. balance = self.trading_engine.get_balance()
  367. current_balance = 0
  368. if balance and balance.get('total'):
  369. current_balance = float(balance['total'].get('USDC', 0))
  370. stats = self.trading_engine.get_stats()
  371. if stats:
  372. stats_message = stats.format_stats_message(current_balance)
  373. await context.bot.send_message(chat_id=chat_id, text=stats_message, parse_mode='HTML')
  374. else:
  375. await context.bot.send_message(chat_id=chat_id, text="โŒ Could not load trading statistics")
  376. async def trades_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  377. """Handle the /trades command - Show recent trade history."""
  378. if not self._is_authorized(update):
  379. return
  380. try:
  381. stats = self.trading_engine.get_stats()
  382. if not stats:
  383. await update.message.reply_text("โŒ Trading statistics not available.", parse_mode='HTML')
  384. return
  385. # Get recent trades (limit to last 20)
  386. recent_trades = stats.get_recent_trades(limit=20)
  387. if not recent_trades:
  388. await update.message.reply_text("๐Ÿ“Š <b>No trades found.</b>", parse_mode='HTML')
  389. return
  390. message = "๐Ÿ“ˆ <b>Recent Trades (Last 20)</b>\n\n"
  391. for trade in recent_trades:
  392. symbol = trade['symbol']
  393. token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0] if ':' in symbol else symbol
  394. side = trade['side'].upper()
  395. amount = trade['amount']
  396. price = trade['price']
  397. timestamp = trade['timestamp']
  398. pnl = trade.get('realized_pnl', 0)
  399. # Format timestamp
  400. try:
  401. from datetime import datetime
  402. dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
  403. time_str = dt.strftime('%m/%d %H:%M')
  404. except:
  405. time_str = "Unknown"
  406. # PnL emoji and formatting
  407. if pnl > 0:
  408. pnl_emoji = "๐ŸŸข"
  409. pnl_str = f"+${pnl:.2f}"
  410. elif pnl < 0:
  411. pnl_emoji = "๐Ÿ”ด"
  412. pnl_str = f"${pnl:.2f}"
  413. else:
  414. pnl_emoji = "โšช"
  415. pnl_str = "$0.00"
  416. side_emoji = "๐ŸŸข" if side == 'BUY' else "๐Ÿ”ด"
  417. message += f"{side_emoji} <b>{side}</b> {amount} {token} @ ${price:,.2f}\n"
  418. message += f" {pnl_emoji} P&L: {pnl_str} | {time_str}\n\n"
  419. await update.message.reply_text(message, parse_mode='HTML')
  420. except Exception as e:
  421. logger.error(f"Error in trades command: {e}")
  422. await update.message.reply_text("โŒ Error retrieving trade history.", parse_mode='HTML')
  423. async def cycles_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  424. """Handle the /cycles command - Show trade cycles and lifecycle statistics."""
  425. if not self._is_authorized(update):
  426. return
  427. try:
  428. stats = self.trading_engine.get_stats()
  429. if not stats:
  430. await update.message.reply_text("โŒ Trading statistics not available.", parse_mode='HTML')
  431. return
  432. # Get trade cycle performance stats
  433. cycle_stats = stats.get_trade_cycle_performance_stats()
  434. if not cycle_stats or cycle_stats.get('total_closed_trades', 0) == 0:
  435. await update.message.reply_text("๐Ÿ“Š <b>No completed trade cycles found.</b>", parse_mode='HTML')
  436. return
  437. # Get recent trade cycles
  438. recent_cycles = stats.get_recent_trade_cycles(limit=10)
  439. open_cycles = stats.get_open_trade_cycles()
  440. message = "๐Ÿ”„ <b>Trade Cycle Statistics</b>\n\n"
  441. # Performance summary
  442. total_trades = cycle_stats.get('total_closed_trades', 0)
  443. win_rate = cycle_stats.get('win_rate', 0)
  444. total_pnl = cycle_stats.get('total_pnl', 0)
  445. avg_duration = cycle_stats.get('avg_duration_minutes', 0)
  446. profit_factor = cycle_stats.get('profit_factor', 0)
  447. stop_loss_rate = cycle_stats.get('stop_loss_rate', 0)
  448. pnl_emoji = "๐ŸŸข" if total_pnl >= 0 else "๐Ÿ”ด"
  449. message += f"๐Ÿ“Š <b>Performance Summary:</b>\n"
  450. message += f"โ€ข Total Completed: {total_trades} trades\n"
  451. message += f"โ€ข Win Rate: {win_rate:.1f}%\n"
  452. message += f"โ€ข {pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
  453. message += f"โ€ข Avg Duration: {avg_duration:.1f} min\n"
  454. message += f"โ€ข Profit Factor: {profit_factor:.2f}\n"
  455. message += f"โ€ข Stop Loss Rate: {stop_loss_rate:.1f}%\n\n"
  456. # Open cycles
  457. if open_cycles:
  458. message += f"๐ŸŸข <b>Open Cycles ({len(open_cycles)}):</b>\n"
  459. for cycle in open_cycles[:5]: # Show max 5
  460. symbol = cycle['symbol']
  461. token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
  462. side = cycle['side'].upper()
  463. entry_price = cycle.get('entry_price', 0)
  464. side_emoji = "๐Ÿ“ˆ" if side == 'BUY' else "๐Ÿ“‰"
  465. message += f"{side_emoji} {side} {token} @ ${entry_price:.2f}\n"
  466. message += "\n"
  467. # Recent completed cycles
  468. if recent_cycles:
  469. completed_recent = [c for c in recent_cycles if c['status'] == 'closed'][:5]
  470. if completed_recent:
  471. message += f"๐Ÿ“‹ <b>Recent Completed ({len(completed_recent)}):</b>\n"
  472. for cycle in completed_recent:
  473. symbol = cycle['symbol']
  474. token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
  475. side = cycle['side'].upper()
  476. entry_price = cycle.get('entry_price', 0)
  477. exit_price = cycle.get('exit_price', 0)
  478. pnl = cycle.get('realized_pnl', 0)
  479. exit_type = cycle.get('exit_type', 'unknown')
  480. duration = cycle.get('duration_seconds', 0)
  481. # Format duration
  482. if duration > 3600:
  483. duration_str = f"{duration//3600:.0f}h"
  484. elif duration > 60:
  485. duration_str = f"{duration//60:.0f}m"
  486. else:
  487. duration_str = f"{duration}s"
  488. pnl_emoji = "๐ŸŸข" if pnl >= 0 else "๐Ÿ”ด"
  489. side_emoji = "๐Ÿ“ˆ" if side == 'BUY' else "๐Ÿ“‰"
  490. exit_emoji = "๐Ÿ›‘" if exit_type == 'stop_loss' else "๐ŸŽฏ" if exit_type == 'take_profit' else "๐Ÿ‘‹"
  491. message += f"{side_emoji} {side} {token}: ${entry_price:.2f} โ†’ ${exit_price:.2f}\n"
  492. message += f" {pnl_emoji} ${pnl:+.2f} | {exit_emoji} {exit_type} | {duration_str}\n"
  493. message += "\n"
  494. message += "๐Ÿ’ก Trade cycles track complete trades from open to close with full P&L analysis."
  495. await update.message.reply_text(message, parse_mode='HTML')
  496. except Exception as e:
  497. error_message = f"โŒ Error processing cycles command: {str(e)}"
  498. await context.bot.send_message(chat_id=chat_id, text=error_message)
  499. logger.error(f"Error in cycles command: {e}")
  500. async def active_trades_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  501. """Handle the /active command to show active trades (Phase 1 testing)."""
  502. chat_id = update.effective_chat.id
  503. if not self._is_authorized(chat_id):
  504. await context.bot.send_message(chat_id=chat_id, text="โŒ Unauthorized access.")
  505. return
  506. try:
  507. stats = self.trading_engine.get_stats()
  508. if not stats:
  509. await context.bot.send_message(chat_id=chat_id, text="โŒ Could not load trading statistics")
  510. return
  511. # Get all active trades
  512. all_active_trades = stats.get_all_active_trades()
  513. if not all_active_trades:
  514. await context.bot.send_message(
  515. chat_id=chat_id,
  516. text="๐Ÿ“Š <b>Active Trades (Phase 1)</b>\n\n๐Ÿ“ญ No active trades found.",
  517. parse_mode='HTML'
  518. )
  519. return
  520. # Group by status
  521. active_trades_by_status = {}
  522. for trade in all_active_trades:
  523. status = trade['status']
  524. if status not in active_trades_by_status:
  525. active_trades_by_status[status] = []
  526. active_trades_by_status[status].append(trade)
  527. message_text = "๐Ÿ“Š <b>Active Trades (Phase 1)</b>\n\n"
  528. # Show each status group
  529. for status, trades in active_trades_by_status.items():
  530. status_emoji = {
  531. 'pending': 'โณ',
  532. 'active': '๐ŸŸข',
  533. 'closed': 'โœ…',
  534. 'cancelled': 'โŒ'
  535. }.get(status, '๐Ÿ“Š')
  536. message_text += f"{status_emoji} <b>{status.upper()}</b> ({len(trades)} trades):\n"
  537. for trade in trades[:5]: # Limit to 5 per status to avoid long messages
  538. symbol = trade['symbol']
  539. token = symbol.split('/')[0] if '/' in symbol else symbol
  540. side = trade['side'].upper()
  541. entry_price = trade.get('entry_price')
  542. entry_amount = trade.get('entry_amount')
  543. realized_pnl = trade.get('realized_pnl', 0)
  544. message_text += f" โ€ข {side} {token}"
  545. if entry_price and entry_amount:
  546. message_text += f" | {entry_amount:.6f} @ ${entry_price:.2f}"
  547. if status == 'closed' and realized_pnl != 0:
  548. pnl_emoji = "๐ŸŸข" if realized_pnl >= 0 else "๐Ÿ”ด"
  549. message_text += f" | {pnl_emoji} ${realized_pnl:.2f}"
  550. if trade.get('stop_loss_price'):
  551. message_text += f" | SL: ${trade['stop_loss_price']:.2f}"
  552. message_text += "\n"
  553. if len(trades) > 5:
  554. message_text += f" ... and {len(trades) - 5} more\n"
  555. message_text += "\n"
  556. # Add summary
  557. total_trades = len(all_active_trades)
  558. pending_count = len(active_trades_by_status.get('pending', []))
  559. active_count = len(active_trades_by_status.get('active', []))
  560. closed_count = len(active_trades_by_status.get('closed', []))
  561. cancelled_count = len(active_trades_by_status.get('cancelled', []))
  562. message_text += f"๐Ÿ“ˆ <b>Summary:</b>\n"
  563. message_text += f" Total: {total_trades} | "
  564. message_text += f"Pending: {pending_count} | "
  565. message_text += f"Active: {active_count} | "
  566. message_text += f"Closed: {closed_count} | "
  567. message_text += f"Cancelled: {cancelled_count}\n\n"
  568. message_text += f"๐Ÿ’ก This is Phase 1 testing - active trades run parallel to trade cycles"
  569. await context.bot.send_message(chat_id=chat_id, text=message_text.strip(), parse_mode='HTML')
  570. except Exception as e:
  571. error_message = f"โŒ Error processing active trades command: {str(e)}"
  572. await context.bot.send_message(chat_id=chat_id, text=error_message)
  573. logger.error(f"Error in active trades command: {e}")
  574. async def market_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  575. """Handle the /market command."""
  576. chat_id = update.effective_chat.id
  577. if not self._is_authorized(chat_id):
  578. await context.bot.send_message(chat_id=chat_id, text="โŒ Unauthorized access.")
  579. return
  580. # Get token from arguments or use default
  581. if context.args and len(context.args) > 0:
  582. token = context.args[0].upper()
  583. else:
  584. token = Config.DEFAULT_TRADING_TOKEN
  585. symbol = f"{token}/USDC:USDC"
  586. market_data = self.trading_engine.get_market_data(symbol)
  587. if market_data:
  588. ticker = market_data.get('ticker', {})
  589. current_price = float(ticker.get('last', 0.0) or 0.0)
  590. bid_price = float(ticker.get('bid', 0.0) or 0.0)
  591. ask_price = float(ticker.get('ask', 0.0) or 0.0)
  592. raw_base_volume = ticker.get('baseVolume')
  593. volume_24h = float(raw_base_volume if raw_base_volume is not None else 0.0)
  594. raw_change_24h = ticker.get('change')
  595. change_24h = float(raw_change_24h if raw_change_24h is not None else 0.0)
  596. raw_percentage = ticker.get('percentage')
  597. change_percent = float(raw_percentage if raw_percentage is not None else 0.0)
  598. high_24h = float(ticker.get('high', 0.0) or 0.0)
  599. low_24h = float(ticker.get('low', 0.0) or 0.0)
  600. # Market direction emoji
  601. trend_emoji = "๐ŸŸข" if change_24h >= 0 else "๐Ÿ”ด"
  602. # Format prices with proper precision for this token
  603. formatter = get_formatter()
  604. current_price_str = formatter.format_price_with_symbol(current_price, token)
  605. bid_price_str = formatter.format_price_with_symbol(bid_price, token)
  606. ask_price_str = formatter.format_price_with_symbol(ask_price, token)
  607. spread_str = formatter.format_price_with_symbol(ask_price - bid_price, token)
  608. high_24h_str = formatter.format_price_with_symbol(high_24h, token)
  609. low_24h_str = formatter.format_price_with_symbol(low_24h, token)
  610. change_24h_str = formatter.format_price_with_symbol(change_24h, token)
  611. market_text = f"""
  612. ๐Ÿ“Š <b>{token} Market Data</b>
  613. ๐Ÿ’ฐ <b>Price Information:</b>
  614. ๐Ÿ’ต Current: {current_price_str}
  615. ๐ŸŸข Bid: {bid_price_str}
  616. ๐Ÿ”ด Ask: {ask_price_str}
  617. ๐Ÿ“Š Spread: {spread_str}
  618. ๐Ÿ“ˆ <b>24h Statistics:</b>
  619. {trend_emoji} Change: {change_24h_str} ({change_percent:+.2f}%)
  620. ๐Ÿ” High: {high_24h_str}
  621. ๐Ÿ”ป Low: {low_24h_str}
  622. ๐Ÿ“Š Volume: {volume_24h:,.2f} {token}
  623. โฐ <b>Last Updated:</b> {datetime.now().strftime('%H:%M:%S')}
  624. """
  625. await context.bot.send_message(chat_id=chat_id, text=market_text.strip(), parse_mode='HTML')
  626. else:
  627. await context.bot.send_message(chat_id=chat_id, text=f"โŒ Could not fetch market data for {token}")
  628. async def price_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  629. """Handle the /price command."""
  630. chat_id = update.effective_chat.id
  631. if not self._is_authorized(chat_id):
  632. await context.bot.send_message(chat_id=chat_id, text="โŒ Unauthorized access.")
  633. return
  634. # Get token from arguments or use default
  635. if context.args and len(context.args) > 0:
  636. token = context.args[0].upper()
  637. else:
  638. token = Config.DEFAULT_TRADING_TOKEN
  639. symbol = f"{token}/USDC:USDC"
  640. market_data = self.trading_engine.get_market_data(symbol)
  641. if market_data:
  642. ticker = market_data.get('ticker', {})
  643. current_price = float(ticker.get('last', 0.0) or 0.0)
  644. raw_change_24h = ticker.get('change')
  645. change_24h = float(raw_change_24h if raw_change_24h is not None else 0.0)
  646. raw_percentage = ticker.get('percentage')
  647. change_percent = float(raw_percentage if raw_percentage is not None else 0.0)
  648. # Price direction emoji
  649. trend_emoji = "๐ŸŸข" if change_24h >= 0 else "๐Ÿ”ด"
  650. # Format prices with proper precision for this token
  651. formatter = get_formatter()
  652. current_price_str = formatter.format_price_with_symbol(current_price, token)
  653. change_24h_str = formatter.format_price_with_symbol(change_24h, token)
  654. price_text = f"""
  655. ๐Ÿ’ต <b>{token} Price</b>
  656. ๐Ÿ’ฐ {current_price_str}
  657. {trend_emoji} {change_percent:+.2f}% ({change_24h_str})
  658. โฐ {datetime.now().strftime('%H:%M:%S')}
  659. """
  660. await context.bot.send_message(chat_id=chat_id, text=price_text.strip(), parse_mode='HTML')
  661. else:
  662. await context.bot.send_message(chat_id=chat_id, text=f"โŒ Could not fetch price for {token}")
  663. async def performance_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  664. """Handle the /performance command to show token performance ranking or detailed stats."""
  665. chat_id = update.effective_chat.id
  666. if not self._is_authorized(chat_id):
  667. await context.bot.send_message(chat_id=chat_id, text="โŒ Unauthorized access.")
  668. return
  669. try:
  670. # Check if specific token is requested
  671. if context.args and len(context.args) >= 1:
  672. # Detailed performance for specific token
  673. token = context.args[0].upper()
  674. await self._show_token_performance(chat_id, token, context)
  675. else:
  676. # Show token performance ranking
  677. await self._show_performance_ranking(chat_id, context)
  678. except Exception as e:
  679. error_message = f"โŒ Error processing performance command: {str(e)}"
  680. await context.bot.send_message(chat_id=chat_id, text=error_message)
  681. logger.error(f"Error in performance command: {e}")
  682. async def _show_performance_ranking(self, chat_id: str, context: ContextTypes.DEFAULT_TYPE):
  683. """Show token performance ranking (compressed view)."""
  684. stats = self.trading_engine.get_stats()
  685. if not stats:
  686. await context.bot.send_message(chat_id=chat_id, text="โŒ Could not load trading statistics")
  687. return
  688. token_performance = stats.get_token_performance()
  689. if not token_performance:
  690. await context.bot.send_message(chat_id=chat_id, text=
  691. "๐Ÿ“Š <b>Token Performance</b>\n\n"
  692. "๐Ÿ“ญ No trading data available yet.\n\n"
  693. "๐Ÿ’ก Performance tracking starts after your first completed trades.\n"
  694. "Use /long or /short to start trading!",
  695. parse_mode='HTML'
  696. )
  697. return
  698. # Sort tokens by total P&L (best to worst)
  699. sorted_tokens = sorted(
  700. token_performance.items(),
  701. key=lambda x: x[1]['total_pnl'],
  702. reverse=True
  703. )
  704. performance_text = "๐Ÿ† <b>Token Performance Ranking</b>\n\n"
  705. # Add ranking with emojis
  706. for i, (token, stats_data) in enumerate(sorted_tokens, 1):
  707. # Ranking emoji
  708. if i == 1:
  709. rank_emoji = "๐Ÿฅ‡"
  710. elif i == 2:
  711. rank_emoji = "๐Ÿฅˆ"
  712. elif i == 3:
  713. rank_emoji = "๐Ÿฅ‰"
  714. else:
  715. rank_emoji = f"#{i}"
  716. # P&L emoji
  717. pnl_emoji = "๐ŸŸข" if stats_data['total_pnl'] >= 0 else "๐Ÿ”ด"
  718. # Format the line
  719. performance_text += f"{rank_emoji} <b>{token}</b>\n"
  720. performance_text += f" {pnl_emoji} P&L: ${stats_data['total_pnl']:,.2f} ({stats_data['pnl_percentage']:+.1f}%)\n"
  721. performance_text += f" ๐Ÿ“Š Trades: {stats_data['completed_trades']}"
  722. # Add win rate if there are completed trades
  723. if stats_data['completed_trades'] > 0:
  724. performance_text += f" | Win: {stats_data['win_rate']:.0f}%"
  725. performance_text += "\n\n"
  726. # Add summary
  727. total_pnl = sum(stats_data['total_pnl'] for stats_data in token_performance.values())
  728. total_trades = sum(stats_data['completed_trades'] for stats_data in token_performance.values())
  729. total_pnl_emoji = "๐ŸŸข" if total_pnl >= 0 else "๐Ÿ”ด"
  730. performance_text += f"๐Ÿ’ผ <b>Portfolio Summary:</b>\n"
  731. performance_text += f" {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
  732. performance_text += f" ๐Ÿ“ˆ Tokens Traded: {len(token_performance)}\n"
  733. performance_text += f" ๐Ÿ”„ Completed Trades: {total_trades}\n\n"
  734. performance_text += f"๐Ÿ’ก <b>Usage:</b> <code>/performance BTC</code> for detailed {Config.DEFAULT_TRADING_TOKEN} stats"
  735. await context.bot.send_message(chat_id=chat_id, text=performance_text.strip(), parse_mode='HTML')
  736. async def _show_token_performance(self, chat_id: str, token: str, context: ContextTypes.DEFAULT_TYPE):
  737. """Show detailed performance for a specific token."""
  738. stats = self.trading_engine.get_stats()
  739. if not stats:
  740. await context.bot.send_message(chat_id=chat_id, text="โŒ Could not load trading statistics")
  741. return
  742. token_stats = stats.get_token_detailed_stats(token)
  743. # Check if token has any data
  744. if token_stats.get('total_trades', 0) == 0:
  745. await context.bot.send_message(chat_id=chat_id, text=
  746. f"๐Ÿ“Š <b>{token} Performance</b>\n\n"
  747. f"๐Ÿ“ญ No trading history found for {token}.\n\n"
  748. f"๐Ÿ’ก Start trading {token} with:\n"
  749. f"โ€ข <code>/long {token} 100</code>\n"
  750. f"โ€ข <code>/short {token} 100</code>\n\n"
  751. f"๐Ÿ”„ Use <code>/performance</code> to see all token rankings.",
  752. parse_mode='HTML'
  753. )
  754. return
  755. # Check if there's a message (no completed trades)
  756. if 'message' in token_stats and token_stats.get('completed_trades', 0) == 0:
  757. await context.bot.send_message(chat_id=chat_id, text=
  758. f"๐Ÿ“Š <b>{token} Performance</b>\n\n"
  759. f"{token_stats['message']}\n\n"
  760. f"๐Ÿ“ˆ <b>Current Activity:</b>\n"
  761. f"โ€ข Total Trades: {token_stats['total_trades']}\n"
  762. f"โ€ข Buy Orders: {token_stats.get('buy_trades', 0)}\n"
  763. f"โ€ข Sell Orders: {token_stats.get('sell_trades', 0)}\n"
  764. f"โ€ข Volume: ${token_stats.get('total_volume', 0):,.2f}\n\n"
  765. f"๐Ÿ’ก Complete some trades to see P&L statistics!\n"
  766. f"๐Ÿ”„ Use <code>/performance</code> to see all token rankings.",
  767. parse_mode='HTML'
  768. )
  769. return
  770. # Detailed stats display
  771. pnl_emoji = "๐ŸŸข" if token_stats['total_pnl'] >= 0 else "๐Ÿ”ด"
  772. performance_text = f"""
  773. ๐Ÿ“Š <b>{token} Detailed Performance</b>
  774. ๐Ÿ’ฐ <b>P&L Summary:</b>
  775. โ€ข {pnl_emoji} Total P&L: ${token_stats['total_pnl']:,.2f} ({token_stats['pnl_percentage']:+.2f}%)
  776. โ€ข ๐Ÿ’ต Total Volume: ${token_stats['completed_volume']:,.2f}
  777. โ€ข ๐Ÿ“ˆ Expectancy: ${token_stats['expectancy']:,.2f}
  778. ๐Ÿ“Š <b>Trading Activity:</b>
  779. โ€ข Total Trades: {token_stats['total_trades']}
  780. โ€ข Completed: {token_stats['completed_trades']}
  781. โ€ข Buy Orders: {token_stats['buy_trades']}
  782. โ€ข Sell Orders: {token_stats['sell_trades']}
  783. ๐Ÿ† <b>Performance Metrics:</b>
  784. โ€ข Win Rate: {token_stats['win_rate']:.1f}%
  785. โ€ข Profit Factor: {token_stats['profit_factor']:.2f}
  786. โ€ข Wins: {token_stats['total_wins']} | Losses: {token_stats['total_losses']}
  787. ๐Ÿ’ก <b>Best/Worst:</b>
  788. โ€ข Largest Win: ${token_stats['largest_win']:,.2f}
  789. โ€ข Largest Loss: ${token_stats['largest_loss']:,.2f}
  790. โ€ข Avg Win: ${token_stats['avg_win']:,.2f}
  791. โ€ข Avg Loss: ${token_stats['avg_loss']:,.2f}
  792. """
  793. # Add recent trades if available
  794. if token_stats.get('recent_trades'):
  795. performance_text += f"\n๐Ÿ”„ <b>Recent Trades:</b>\n"
  796. for trade in token_stats['recent_trades'][-3:]: # Last 3 trades
  797. trade_time = datetime.fromisoformat(trade['timestamp']).strftime('%m/%d %H:%M')
  798. side_emoji = "๐ŸŸข" if trade['side'] == 'buy' else "๐Ÿ”ด"
  799. pnl_display = f" | P&L: ${trade.get('pnl', 0):.2f}" if trade.get('pnl', 0) != 0 else ""
  800. performance_text += f"โ€ข {side_emoji} {trade['side'].upper()} ${trade['value']:,.0f} @ {trade_time}{pnl_display}\n"
  801. performance_text += f"\n๐Ÿ”„ Use <code>/performance</code> to see all token rankings"
  802. await context.bot.send_message(chat_id=chat_id, text=performance_text.strip(), parse_mode='HTML')
  803. async def daily_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  804. """Handle the /daily command to show daily performance stats."""
  805. chat_id = update.effective_chat.id
  806. if not self._is_authorized(chat_id):
  807. await context.bot.send_message(chat_id=chat_id, text="โŒ Unauthorized access.")
  808. return
  809. try:
  810. stats = self.trading_engine.get_stats()
  811. if not stats:
  812. await context.bot.send_message(chat_id=chat_id, text="โŒ Could not load trading statistics")
  813. return
  814. daily_stats = stats.get_daily_stats(10)
  815. if not daily_stats:
  816. await context.bot.send_message(chat_id=chat_id, text=
  817. "๐Ÿ“… <b>Daily Performance</b>\n\n"
  818. "๐Ÿ“ญ No daily performance data available yet.\n\n"
  819. "๐Ÿ’ก Daily stats are calculated from completed trades.\n"
  820. "Start trading to see daily performance!",
  821. parse_mode='HTML'
  822. )
  823. return
  824. daily_text = "๐Ÿ“… <b>Daily Performance (Last 10 Days)</b>\n\n"
  825. total_pnl = 0
  826. total_trades = 0
  827. trading_days = 0
  828. for day_stats in daily_stats:
  829. if day_stats['has_trades']:
  830. # Day with completed trades
  831. pnl_emoji = "๐ŸŸข" if day_stats['pnl'] >= 0 else "๐Ÿ”ด"
  832. daily_text += f"๐Ÿ“Š <b>{day_stats['date_formatted']}</b>\n"
  833. daily_text += f" {pnl_emoji} P&L: ${day_stats['pnl']:,.2f} ({day_stats['pnl_pct']:+.1f}%)\n"
  834. daily_text += f" ๐Ÿ”„ Trades: {day_stats['trades']}\n\n"
  835. total_pnl += day_stats['pnl']
  836. total_trades += day_stats['trades']
  837. trading_days += 1
  838. else:
  839. # Day with no trades
  840. daily_text += f"๐Ÿ“Š <b>{day_stats['date_formatted']}</b>\n"
  841. daily_text += f" ๐Ÿ“ญ No trading activity\n\n"
  842. # Add summary
  843. if trading_days > 0:
  844. avg_daily_pnl = total_pnl / trading_days
  845. avg_pnl_emoji = "๐ŸŸข" if avg_daily_pnl >= 0 else "๐Ÿ”ด"
  846. daily_text += f"๐Ÿ“ˆ <b>Period Summary:</b>\n"
  847. daily_text += f" {avg_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
  848. daily_text += f" ๐Ÿ“Š Trading Days: {trading_days}/10\n"
  849. daily_text += f" ๐Ÿ“ˆ Avg Daily P&L: ${avg_daily_pnl:,.2f}\n"
  850. daily_text += f" ๐Ÿ”„ Total Trades: {total_trades}\n"
  851. await context.bot.send_message(chat_id=chat_id, text=daily_text.strip(), parse_mode='HTML')
  852. except Exception as e:
  853. error_message = f"โŒ Error processing daily command: {str(e)}"
  854. await context.bot.send_message(chat_id=chat_id, text=error_message)
  855. logger.error(f"Error in daily command: {e}")
  856. async def weekly_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  857. """Handle the /weekly command to show weekly performance stats."""
  858. chat_id = update.effective_chat.id
  859. if not self._is_authorized(chat_id):
  860. await context.bot.send_message(chat_id=chat_id, text="โŒ Unauthorized access.")
  861. return
  862. try:
  863. stats = self.trading_engine.get_stats()
  864. if not stats:
  865. await context.bot.send_message(chat_id=chat_id, text="โŒ Could not load trading statistics")
  866. return
  867. weekly_stats = stats.get_weekly_stats(10)
  868. if not weekly_stats:
  869. await context.bot.send_message(chat_id=chat_id, text=
  870. "๐Ÿ“Š <b>Weekly Performance</b>\n\n"
  871. "๐Ÿ“ญ No weekly performance data available yet.\n\n"
  872. "๐Ÿ’ก Weekly stats are calculated from completed trades.\n"
  873. "Start trading to see weekly performance!",
  874. parse_mode='HTML'
  875. )
  876. return
  877. weekly_text = "๐Ÿ“Š <b>Weekly Performance (Last 10 Weeks)</b>\n\n"
  878. total_pnl = 0
  879. total_trades = 0
  880. trading_weeks = 0
  881. for week_stats in weekly_stats:
  882. if week_stats['has_trades']:
  883. # Week with completed trades
  884. pnl_emoji = "๐ŸŸข" if week_stats['pnl'] >= 0 else "๐Ÿ”ด"
  885. weekly_text += f"๐Ÿ“ˆ <b>{week_stats['week_formatted']}</b>\n"
  886. weekly_text += f" {pnl_emoji} P&L: ${week_stats['pnl']:,.2f} ({week_stats['pnl_pct']:+.1f}%)\n"
  887. weekly_text += f" ๐Ÿ”„ Trades: {week_stats['trades']}\n\n"
  888. total_pnl += week_stats['pnl']
  889. total_trades += week_stats['trades']
  890. trading_weeks += 1
  891. else:
  892. # Week with no trades
  893. weekly_text += f"๐Ÿ“ˆ <b>{week_stats['week_formatted']}</b>\n"
  894. weekly_text += f" ๐Ÿ“ญ No completed trades\n\n"
  895. # Add summary
  896. if trading_weeks > 0:
  897. total_pnl_emoji = "๐ŸŸข" if total_pnl >= 0 else "๐Ÿ”ด"
  898. weekly_text += f"๐Ÿ’ผ <b>10-Week Summary:</b>\n"
  899. weekly_text += f" {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
  900. weekly_text += f" ๐Ÿ”„ Total Trades: {total_trades}\n"
  901. weekly_text += f" ๐Ÿ“ˆ Trading Weeks: {trading_weeks}/10\n"
  902. weekly_text += f" ๐Ÿ“Š Avg per Trading Week: ${total_pnl/trading_weeks:,.2f}"
  903. else:
  904. weekly_text += f"๐Ÿ’ผ <b>10-Week Summary:</b>\n"
  905. weekly_text += f" ๐Ÿ“ญ No completed trades in the last 10 weeks\n"
  906. weekly_text += f" ๐Ÿ’ก Start trading to see weekly performance!"
  907. await context.bot.send_message(chat_id=chat_id, text=weekly_text.strip(), parse_mode='HTML')
  908. except Exception as e:
  909. error_message = f"โŒ Error processing weekly command: {str(e)}"
  910. await context.bot.send_message(chat_id=chat_id, text=error_message)
  911. logger.error(f"Error in weekly command: {e}")
  912. async def monthly_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  913. """Handle the /monthly command to show monthly performance stats."""
  914. chat_id = update.effective_chat.id
  915. if not self._is_authorized(chat_id):
  916. await context.bot.send_message(chat_id=chat_id, text="โŒ Unauthorized access.")
  917. return
  918. try:
  919. stats = self.trading_engine.get_stats()
  920. if not stats:
  921. await context.bot.send_message(chat_id=chat_id, text="โŒ Could not load trading statistics")
  922. return
  923. monthly_stats = stats.get_monthly_stats(10)
  924. if not monthly_stats:
  925. await context.bot.send_message(chat_id=chat_id, text=
  926. "๐Ÿ“† <b>Monthly Performance</b>\n\n"
  927. "๐Ÿ“ญ No monthly performance data available yet.\n\n"
  928. "๐Ÿ’ก Monthly stats are calculated from completed trades.\n"
  929. "Start trading to see monthly performance!",
  930. parse_mode='HTML'
  931. )
  932. return
  933. monthly_text = "๐Ÿ“† <b>Monthly Performance (Last 10 Months)</b>\n\n"
  934. total_pnl = 0
  935. total_trades = 0
  936. trading_months = 0
  937. for month_stats in monthly_stats:
  938. if month_stats['has_trades']:
  939. # Month with completed trades
  940. pnl_emoji = "๐ŸŸข" if month_stats['pnl'] >= 0 else "๐Ÿ”ด"
  941. monthly_text += f"๐Ÿ“… <b>{month_stats['month_formatted']}</b>\n"
  942. monthly_text += f" {pnl_emoji} P&L: ${month_stats['pnl']:,.2f} ({month_stats['pnl_pct']:+.1f}%)\n"
  943. monthly_text += f" ๐Ÿ”„ Trades: {month_stats['trades']}\n\n"
  944. total_pnl += month_stats['pnl']
  945. total_trades += month_stats['trades']
  946. trading_months += 1
  947. else:
  948. # Month with no trades
  949. monthly_text += f"๐Ÿ“… <b>{month_stats['month_formatted']}</b>\n"
  950. monthly_text += f" ๐Ÿ“ญ No completed trades\n\n"
  951. # Add summary
  952. if trading_months > 0:
  953. total_pnl_emoji = "๐ŸŸข" if total_pnl >= 0 else "๐Ÿ”ด"
  954. monthly_text += f"๐Ÿ’ผ <b>10-Month Summary:</b>\n"
  955. monthly_text += f" {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
  956. monthly_text += f" ๐Ÿ”„ Total Trades: {total_trades}\n"
  957. monthly_text += f" ๐Ÿ“ˆ Trading Months: {trading_months}/10\n"
  958. monthly_text += f" ๐Ÿ“Š Avg per Trading Month: ${total_pnl/trading_months:,.2f}"
  959. else:
  960. monthly_text += f"๐Ÿ’ผ <b>10-Month Summary:</b>\n"
  961. monthly_text += f" ๐Ÿ“ญ No completed trades in the last 10 months\n"
  962. monthly_text += f" ๐Ÿ’ก Start trading to see monthly performance!"
  963. await context.bot.send_message(chat_id=chat_id, text=monthly_text.strip(), parse_mode='HTML')
  964. except Exception as e:
  965. error_message = f"โŒ Error processing monthly command: {str(e)}"
  966. await context.bot.send_message(chat_id=chat_id, text=error_message)
  967. logger.error(f"Error in monthly command: {e}")
  968. async def risk_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  969. """Handle the /risk command to show advanced risk metrics."""
  970. chat_id = update.effective_chat.id
  971. if not self._is_authorized(chat_id):
  972. await context.bot.send_message(chat_id=chat_id, text="โŒ Unauthorized access.")
  973. return
  974. try:
  975. # Get current balance for context
  976. balance = self.trading_engine.get_balance()
  977. current_balance = 0
  978. if balance and balance.get('total'):
  979. current_balance = float(balance['total'].get('USDC', 0))
  980. # Get risk metrics and basic stats
  981. stats = self.trading_engine.get_stats()
  982. if not stats:
  983. await context.bot.send_message(chat_id=chat_id, text="โŒ Could not load trading statistics")
  984. return
  985. risk_metrics = stats.get_risk_metrics()
  986. basic_stats = stats.get_basic_stats()
  987. # Check if we have enough data for risk calculations
  988. if basic_stats['completed_trades'] < 2:
  989. await context.bot.send_message(chat_id=chat_id, text=
  990. "๐Ÿ“Š <b>Risk Analysis</b>\n\n"
  991. "๐Ÿ“ญ <b>Insufficient Data</b>\n\n"
  992. f"โ€ข Current completed trades: {basic_stats['completed_trades']}\n"
  993. f"โ€ข Required for risk analysis: 2+ trades\n"
  994. f"โ€ข Daily balance snapshots: {len(stats.data.get('daily_balances', []))}\n\n"
  995. "๐Ÿ’ก <b>To enable risk analysis:</b>\n"
  996. "โ€ข Complete more trades to generate returns data\n"
  997. "โ€ข Bot automatically records daily balance snapshots\n"
  998. "โ€ข Risk metrics will be available after sufficient trading history\n\n"
  999. "๐Ÿ“ˆ Use /stats for current performance metrics",
  1000. parse_mode='HTML'
  1001. )
  1002. return
  1003. # Format the risk analysis message
  1004. risk_text = f"""
  1005. ๐Ÿ“Š <b>Risk Analysis & Advanced Metrics</b>
  1006. ๐ŸŽฏ <b>Risk-Adjusted Performance:</b>
  1007. โ€ข Sharpe Ratio: {risk_metrics['sharpe_ratio']:.3f}
  1008. โ€ข Sortino Ratio: {risk_metrics['sortino_ratio']:.3f}
  1009. โ€ข Annual Volatility: {risk_metrics['volatility']:.2f}%
  1010. ๐Ÿ“‰ <b>Drawdown Analysis:</b>
  1011. โ€ข Maximum Drawdown: {risk_metrics['max_drawdown']:.2f}%
  1012. โ€ข Value at Risk (95%): {risk_metrics['var_95']:.2f}%
  1013. ๐Ÿ’ฐ <b>Portfolio Context:</b>
  1014. โ€ข Current Balance: ${current_balance:,.2f}
  1015. โ€ข Initial Balance: ${basic_stats['initial_balance']:,.2f}
  1016. โ€ข Total P&L: ${basic_stats['total_pnl']:,.2f}
  1017. โ€ข Days Active: {basic_stats['days_active']}
  1018. ๐Ÿ“Š <b>Risk Interpretation:</b>
  1019. """
  1020. # Add interpretive guidance
  1021. sharpe = risk_metrics['sharpe_ratio']
  1022. if sharpe > 2.0:
  1023. risk_text += "โ€ข ๐ŸŸข <b>Excellent</b> risk-adjusted returns (Sharpe > 2.0)\n"
  1024. elif sharpe > 1.0:
  1025. risk_text += "โ€ข ๐ŸŸก <b>Good</b> risk-adjusted returns (Sharpe > 1.0)\n"
  1026. elif sharpe > 0.5:
  1027. risk_text += "โ€ข ๐ŸŸ  <b>Moderate</b> risk-adjusted returns (Sharpe > 0.5)\n"
  1028. elif sharpe > 0:
  1029. risk_text += "โ€ข ๐Ÿ”ด <b>Poor</b> risk-adjusted returns (Sharpe > 0)\n"
  1030. else:
  1031. risk_text += "โ€ข โšซ <b>Negative</b> risk-adjusted returns (Sharpe < 0)\n"
  1032. max_dd = risk_metrics['max_drawdown']
  1033. if max_dd < 5:
  1034. risk_text += "โ€ข ๐ŸŸข <b>Low</b> maximum drawdown (< 5%)\n"
  1035. elif max_dd < 15:
  1036. risk_text += "โ€ข ๐ŸŸก <b>Moderate</b> maximum drawdown (< 15%)\n"
  1037. elif max_dd < 30:
  1038. risk_text += "โ€ข ๐ŸŸ  <b>High</b> maximum drawdown (< 30%)\n"
  1039. else:
  1040. risk_text += "โ€ข ๐Ÿ”ด <b>Very High</b> maximum drawdown (> 30%)\n"
  1041. volatility = risk_metrics['volatility']
  1042. if volatility < 10:
  1043. risk_text += "โ€ข ๐ŸŸข <b>Low</b> portfolio volatility (< 10%)\n"
  1044. elif volatility < 25:
  1045. risk_text += "โ€ข ๐ŸŸก <b>Moderate</b> portfolio volatility (< 25%)\n"
  1046. elif volatility < 50:
  1047. risk_text += "โ€ข ๐ŸŸ  <b>High</b> portfolio volatility (< 50%)\n"
  1048. else:
  1049. risk_text += "โ€ข ๐Ÿ”ด <b>Very High</b> portfolio volatility (> 50%)\n"
  1050. risk_text += f"""
  1051. ๐Ÿ’ก <b>Risk Definitions:</b>
  1052. โ€ข <b>Sharpe Ratio:</b> Risk-adjusted return (excess return / volatility)
  1053. โ€ข <b>Sortino Ratio:</b> Return / downside volatility (focuses on bad volatility)
  1054. โ€ข <b>Max Drawdown:</b> Largest peak-to-trough decline
  1055. โ€ข <b>VaR 95%:</b> Maximum expected loss 95% of the time
  1056. โ€ข <b>Volatility:</b> Annualized standard deviation of returns
  1057. ๐Ÿ“ˆ <b>Data Based On:</b>
  1058. โ€ข Completed Trades: {basic_stats['completed_trades']}
  1059. โ€ข Daily Balance Records: {len(stats.data.get('daily_balances', []))}
  1060. โ€ข Trading Period: {basic_stats['days_active']} days
  1061. ๐Ÿ”„ Use /stats for trading performance metrics
  1062. """
  1063. await context.bot.send_message(chat_id=chat_id, text=risk_text.strip(), parse_mode='HTML')
  1064. except Exception as e:
  1065. error_message = f"โŒ Error processing risk command: {str(e)}"
  1066. await context.bot.send_message(chat_id=chat_id, text=error_message)
  1067. logger.error(f"Error in risk command: {e}")
  1068. async def balance_adjustments_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  1069. """Handle the /balance_adjustments command to show deposit/withdrawal history."""
  1070. chat_id = update.effective_chat.id
  1071. if not self._is_authorized(chat_id):
  1072. await context.bot.send_message(chat_id=chat_id, text="โŒ Unauthorized access.")
  1073. return
  1074. try:
  1075. stats = self.trading_engine.get_stats()
  1076. if not stats:
  1077. await context.bot.send_message(chat_id=chat_id, text="โŒ Could not load trading statistics")
  1078. return
  1079. # Get balance adjustments summary
  1080. adjustments_summary = stats.get_balance_adjustments_summary()
  1081. # Get detailed adjustments
  1082. all_adjustments = stats.data.get('balance_adjustments', [])
  1083. if not all_adjustments:
  1084. await context.bot.send_message(chat_id=chat_id, text=
  1085. "๐Ÿ’ฐ <b>Balance Adjustments</b>\n\n"
  1086. "๐Ÿ“ญ No deposits or withdrawals detected yet.\n\n"
  1087. "๐Ÿ’ก The bot automatically monitors for deposits and withdrawals\n"
  1088. "every hour to maintain accurate P&L calculations.",
  1089. parse_mode='HTML'
  1090. )
  1091. return
  1092. # Format the message
  1093. adjustments_text = f"""
  1094. ๐Ÿ’ฐ <b>Balance Adjustments History</b>
  1095. ๐Ÿ“Š <b>Summary:</b>
  1096. โ€ข Total Deposits: ${adjustments_summary['total_deposits']:,.2f}
  1097. โ€ข Total Withdrawals: ${adjustments_summary['total_withdrawals']:,.2f}
  1098. โ€ข Net Adjustment: ${adjustments_summary['net_adjustment']:,.2f}
  1099. โ€ข Total Transactions: {adjustments_summary['adjustment_count']}
  1100. ๐Ÿ“… <b>Recent Adjustments:</b>
  1101. """
  1102. # Show last 10 adjustments
  1103. recent_adjustments = sorted(all_adjustments, key=lambda x: x['timestamp'], reverse=True)[:10]
  1104. for adj in recent_adjustments:
  1105. try:
  1106. # Format timestamp
  1107. adj_time = datetime.fromisoformat(adj['timestamp']).strftime('%m/%d %H:%M')
  1108. # Format type and amount
  1109. if adj['type'] == 'deposit':
  1110. emoji = "๐Ÿ’ฐ"
  1111. amount_str = f"+${adj['amount']:,.2f}"
  1112. else: # withdrawal
  1113. emoji = "๐Ÿ’ธ"
  1114. amount_str = f"-${abs(adj['amount']):,.2f}"
  1115. adjustments_text += f"โ€ข {emoji} {adj_time}: {amount_str}\n"
  1116. except Exception as adj_error:
  1117. logger.warning(f"Error formatting adjustment: {adj_error}")
  1118. continue
  1119. adjustments_text += f"""
  1120. ๐Ÿ’ก <b>How it Works:</b>
  1121. โ€ข Bot checks for deposits/withdrawals every hour
  1122. โ€ข Adjustments maintain accurate P&L calculations
  1123. โ€ข Non-trading balance changes don't affect performance metrics
  1124. โ€ข Trading statistics remain pure and accurate
  1125. โฐ <b>Last Check:</b> {adjustments_summary['last_adjustment'][:16] if adjustments_summary['last_adjustment'] else 'Never'}
  1126. """
  1127. await context.bot.send_message(chat_id=chat_id, text=adjustments_text.strip(), parse_mode='HTML')
  1128. except Exception as e:
  1129. error_message = f"โŒ Error processing balance adjustments command: {str(e)}"
  1130. await context.bot.send_message(chat_id=chat_id, text=error_message)
  1131. logger.error(f"Error in balance_adjustments command: {e}")
  1132. async def commands_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  1133. """Handle the /commands and /c command with quick action buttons."""
  1134. chat_id = update.effective_chat.id
  1135. if not self._is_authorized(chat_id):
  1136. await context.bot.send_message(chat_id=chat_id, text="โŒ Unauthorized access.")
  1137. return
  1138. commands_text = """
  1139. ๐Ÿ“ฑ <b>Quick Commands</b>
  1140. Tap any button below for instant access to bot functions:
  1141. ๐Ÿ’ก <b>Pro Tip:</b> These buttons work the same as typing the commands manually, but faster!
  1142. """
  1143. from telegram import InlineKeyboardButton, InlineKeyboardMarkup
  1144. keyboard = [
  1145. [
  1146. InlineKeyboardButton("๐Ÿ’ฐ Balance", callback_data="/balance"),
  1147. InlineKeyboardButton("๐Ÿ“ˆ Positions", callback_data="/positions")
  1148. ],
  1149. [
  1150. InlineKeyboardButton("๐Ÿ“‹ Orders", callback_data="/orders"),
  1151. InlineKeyboardButton("๐Ÿ“Š Stats", callback_data="/stats")
  1152. ],
  1153. [
  1154. InlineKeyboardButton("๐Ÿ’ต Price", callback_data="/price"),
  1155. InlineKeyboardButton("๐Ÿ“Š Market", callback_data="/market")
  1156. ],
  1157. [
  1158. InlineKeyboardButton("๐Ÿ† Performance", callback_data="/performance"),
  1159. InlineKeyboardButton("๐Ÿ”” Alarms", callback_data="/alarm")
  1160. ],
  1161. [
  1162. InlineKeyboardButton("๐Ÿ“… Daily", callback_data="/daily"),
  1163. InlineKeyboardButton("๐Ÿ“Š Weekly", callback_data="/weekly")
  1164. ],
  1165. [
  1166. InlineKeyboardButton("๐Ÿ“† Monthly", callback_data="/monthly"),
  1167. InlineKeyboardButton("๐Ÿ”„ Trades", callback_data="/trades")
  1168. ],
  1169. [
  1170. InlineKeyboardButton("๐Ÿ”„ Monitoring", callback_data="/monitoring"),
  1171. InlineKeyboardButton("๐Ÿ“ Logs", callback_data="/logs")
  1172. ],
  1173. [
  1174. InlineKeyboardButton("โš™๏ธ Help", callback_data="/help")
  1175. ]
  1176. ]
  1177. reply_markup = InlineKeyboardMarkup(keyboard)
  1178. await context.bot.send_message(chat_id=chat_id, text=commands_text, parse_mode='HTML', reply_markup=reply_markup)
  1179. async def _estimate_entry_price_for_orphaned_position(self, symbol: str, contracts: float) -> float:
  1180. """Estimate entry price for an orphaned position by checking recent fills and market data."""
  1181. try:
  1182. # Method 1: Check recent fills from the exchange
  1183. recent_fills = self.trading_engine.get_recent_fills()
  1184. if recent_fills:
  1185. # Look for recent fills for this symbol
  1186. symbol_fills = [fill for fill in recent_fills if fill.get('symbol') == symbol]
  1187. if symbol_fills:
  1188. # Get the most recent fill as entry price estimate
  1189. latest_fill = symbol_fills[0] # Assuming sorted by newest first
  1190. fill_price = float(latest_fill.get('price', 0))
  1191. if fill_price > 0:
  1192. logger.info(f"๐Ÿ’ก Found recent fill price for {symbol}: ${fill_price:.4f}")
  1193. return fill_price
  1194. # Method 2: Use current market price as fallback
  1195. market_data = self.trading_engine.get_market_data(symbol)
  1196. if market_data and market_data.get('ticker'):
  1197. current_price = float(market_data['ticker'].get('last', 0))
  1198. if current_price > 0:
  1199. logger.warning(f"โš ๏ธ Using current market price as entry estimate for {symbol}: ${current_price:.4f}")
  1200. return current_price
  1201. # Method 3: Last resort - try bid/ask average
  1202. if market_data and market_data.get('ticker'):
  1203. bid = float(market_data['ticker'].get('bid', 0))
  1204. ask = float(market_data['ticker'].get('ask', 0))
  1205. if bid > 0 and ask > 0:
  1206. avg_price = (bid + ask) / 2
  1207. logger.warning(f"โš ๏ธ Using bid/ask average as entry estimate for {symbol}: ${avg_price:.4f}")
  1208. return avg_price
  1209. # Method 4: Absolute fallback - return a small positive value to avoid 0
  1210. logger.error(f"โŒ Could not estimate entry price for {symbol}, using fallback value of $1.00")
  1211. return 1.0
  1212. except Exception as e:
  1213. logger.error(f"โŒ Error estimating entry price for {symbol}: {e}")
  1214. return 1.0 # Safe fallback