12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481 |
- #!/usr/bin/env python3
- """
- Info Commands - Handles information-related Telegram commands.
- """
- import logging
- from datetime import datetime
- from typing import Optional, Dict, Any, List
- from telegram import Update
- from telegram.ext import ContextTypes
- from src.config.config import Config
- from src.utils.price_formatter import format_price_with_symbol, get_formatter
- logger = logging.getLogger(__name__)
- class InfoCommands:
- """Handles all information-related Telegram commands."""
-
- def __init__(self, trading_engine, notification_manager=None):
- """Initialize with trading engine and notification manager."""
- self.trading_engine = trading_engine
- self.notification_manager = notification_manager
-
- def _is_authorized(self, chat_id: str) -> bool:
- """Check if the chat ID is authorized."""
- return str(chat_id) == str(Config.TELEGRAM_CHAT_ID)
-
- async def balance_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /balance command."""
- chat_id = update.effective_chat.id
- if not self._is_authorized(chat_id):
- await context.bot.send_message(chat_id=chat_id, text="โ Unauthorized access.")
- return
-
- balance = self.trading_engine.get_balance()
- if balance:
- balance_text = "๐ฐ <b>Account Balance</b>\n\n"
-
- # Debug: Show raw balance structure (can be removed after debugging)
- logger.debug(f"Raw balance data: {balance}")
-
- # CCXT balance structure includes 'free', 'used', and 'total'
- total_balance = balance.get('total', {})
- free_balance = balance.get('free', {})
- used_balance = balance.get('used', {})
-
- # Get total portfolio value
- total_portfolio_value = 0
-
- # Show USDC balance prominently
- if 'USDC' in total_balance:
- usdc_total = float(total_balance['USDC'])
- usdc_free = float(free_balance.get('USDC', 0))
- usdc_used = float(used_balance.get('USDC', 0))
-
- balance_text += f"๐ต <b>USDC:</b>\n"
- balance_text += f" ๐ Total: ${usdc_total:,.2f}\n"
- balance_text += f" โ
Available: ${usdc_free:,.2f}\n"
- balance_text += f" ๐ In Use: ${usdc_used:,.2f}\n\n"
-
- total_portfolio_value += usdc_total
-
- # Show other non-zero balances
- other_assets = []
- for asset, amount in total_balance.items():
- if asset != 'USDC' and float(amount) > 0:
- other_assets.append((asset, float(amount)))
-
- if other_assets:
- balance_text += "๐ <b>Other Assets:</b>\n"
- for asset, amount in other_assets:
- free_amount = float(free_balance.get(asset, 0))
- used_amount = float(used_balance.get(asset, 0))
-
- balance_text += f"๐ต <b>{asset}:</b>\n"
- balance_text += f" ๐ Total: {amount:.6f}\n"
- balance_text += f" โ
Available: {free_amount:.6f}\n"
- balance_text += f" ๐ In Use: {used_amount:.6f}\n\n"
-
- # Portfolio summary
- usdc_balance = float(total_balance.get('USDC', 0))
- stats = self.trading_engine.get_stats()
- if stats:
- basic_stats = stats.get_basic_stats()
- initial_balance = basic_stats.get('initial_balance', usdc_balance)
- pnl = usdc_balance - initial_balance
- pnl_percent = (pnl / initial_balance * 100) if initial_balance > 0 else 0
- pnl_emoji = "๐ข" if pnl >= 0 else "๐ด"
-
- balance_text += f"๐ผ <b>Portfolio Summary:</b>\n"
- balance_text += f" ๐ฐ Total Value: ${total_portfolio_value:,.2f}\n"
- balance_text += f" ๐ Available for Trading: ${float(free_balance.get('USDC', 0)):,.2f}\n"
- balance_text += f" ๐ In Active Use: ${float(used_balance.get('USDC', 0)):,.2f}\n\n"
- balance_text += f"๐ <b>Performance:</b>\n"
- balance_text += f" ๐ต Initial: ${initial_balance:,.2f}\n"
- balance_text += f" {pnl_emoji} P&L: ${pnl:,.2f} ({pnl_percent:+.2f}%)\n"
-
- await context.bot.send_message(chat_id=chat_id, text=balance_text, parse_mode='HTML')
- else:
- await context.bot.send_message(chat_id=chat_id, text="โ Could not fetch balance information")
-
- async def positions_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /positions command."""
- chat_id = update.effective_chat.id
- if not self._is_authorized(chat_id):
- await context.bot.send_message(chat_id=chat_id, text="โ Unauthorized access.")
- return
-
- # ๐งน PHASE 4: Use unified trades table as the single source of truth
- stats = self.trading_engine.get_stats()
- if not stats:
- await context.bot.send_message(chat_id=chat_id, text="โ Trading statistics not available.")
- return
-
- # ๐ AUTO-SYNC: Check for positions on exchange that don't have trade lifecycle records
- # Use cached data from MarketMonitor if available (updated every heartbeat)
- if (hasattr(self.trading_engine, 'market_monitor') and
- self.trading_engine.market_monitor and
- hasattr(self.trading_engine.market_monitor, 'get_cached_positions')):
-
- cache_age = self.trading_engine.market_monitor.get_cache_age_seconds()
- if cache_age < 60: # Use cached data if less than 1 minute old
- exchange_positions = self.trading_engine.market_monitor.get_cached_positions() or []
- logger.debug(f"Using cached positions for auto-sync (age: {cache_age:.1f}s)")
- else:
- exchange_positions = self.trading_engine.get_positions() or []
- logger.debug("Using fresh API call for auto-sync (cache too old)")
- else:
- exchange_positions = self.trading_engine.get_positions() or []
- logger.debug("Using fresh API call for auto-sync (no cache available)")
-
- synced_positions = []
-
- for exchange_pos in exchange_positions:
- symbol = exchange_pos.get('symbol')
- contracts = float(exchange_pos.get('contracts', 0))
-
- if symbol and abs(contracts) > 0:
- # Check if we have a trade lifecycle record for this position
- existing_trade = stats.get_trade_by_symbol_and_status(symbol, 'position_opened')
-
- if not existing_trade:
- # ๐จ ORPHANED POSITION: Auto-create trade lifecycle record using exchange data
- entry_price = float(exchange_pos.get('entryPrice', 0))
- position_side = 'long' if contracts > 0 else 'short'
- order_side = 'buy' if contracts > 0 else 'sell'
-
- # โ
Use exchange data - no need to estimate!
- if entry_price > 0:
- logger.info(f"๐ Auto-syncing orphaned position: {symbol} {position_side} {abs(contracts)} @ ${entry_price} (exchange data)")
- else:
- # Fallback only if exchange truly doesn't provide entry price
- entry_price = await self._estimate_entry_price_for_orphaned_position(symbol, contracts)
- logger.warning(f"๐ Auto-syncing orphaned position: {symbol} {position_side} {abs(contracts)} @ ${entry_price} (estimated)")
-
- # Create trade lifecycle for external position
- lifecycle_id = stats.create_trade_lifecycle(
- symbol=symbol,
- side=order_side,
- entry_order_id=f"external_sync_{int(datetime.now().timestamp())}",
- trade_type='external'
- )
-
- if lifecycle_id:
- # Update to position_opened status
- success = stats.update_trade_position_opened(
- lifecycle_id=lifecycle_id,
- entry_price=entry_price,
- entry_amount=abs(contracts),
- exchange_fill_id=f"external_fill_{int(datetime.now().timestamp())}"
- )
-
- if success:
- synced_positions.append(symbol)
- logger.info(f"โ
Successfully synced orphaned position for {symbol}")
-
- # ๐ Send immediate notification for auto-synced position
- token = symbol.split('/')[0] if '/' in symbol else symbol
- unrealized_pnl = float(exchange_pos.get('unrealizedPnl', 0))
- position_value = float(exchange_pos.get('notional', 0))
- liquidation_price = float(exchange_pos.get('liquidationPrice', 0))
- leverage = float(exchange_pos.get('leverage', 1))
- pnl_percentage = float(exchange_pos.get('percentage', 0))
-
- pnl_emoji = "๐ข" if unrealized_pnl >= 0 else "๐ด"
- notification_text = (
- f"๐ <b>Position Auto-Synced</b>\n\n"
- f"๐ฏ Token: {token}\n"
- f"๐ Direction: {position_side.upper()}\n"
- f"๐ Size: {abs(contracts):.6f} {token}\n"
- f"๐ฐ Entry: ${entry_price:,.4f}\n"
- f"๐ต Value: ${position_value:,.2f}\n"
- f"{pnl_emoji} P&L: ${unrealized_pnl:,.2f} ({pnl_percentage:+.2f}%)\n"
- )
-
- if leverage > 1:
- notification_text += f"โก Leverage: {leverage:.1f}x\n"
- if liquidation_price > 0:
- notification_text += f"โ ๏ธ Liquidation: ${liquidation_price:,.2f}\n"
-
- notification_text += (
- f"\n๐ Reason: Position opened outside bot\n"
- f"โฐ Time: {datetime.now().strftime('%H:%M:%S')}\n\n"
- f"โ
Position now tracked in bot\n"
- f"๐ก Use /sl {token} [price] to set stop loss"
- )
-
- # Send notification via trading engine's notification manager
- if self.notification_manager:
- try:
- await self.notification_manager.send_generic_notification(notification_text)
- logger.info(f"๐ค Sent auto-sync notification for {symbol}")
- except Exception as e:
- logger.error(f"โ Failed to send auto-sync notification: {e}")
- else:
- logger.warning(f"โ ๏ธ No notification manager available for auto-sync notification")
- else:
- logger.error(f"โ Failed to sync orphaned position for {symbol}")
- else:
- logger.error(f"โ Failed to create lifecycle for orphaned position {symbol}")
-
- if synced_positions:
- sync_msg = f"๐ <b>Auto-synced {len(synced_positions)} orphaned position(s):</b> {', '.join([s.split('/')[0] for s in synced_positions])}\n\n"
- else:
- sync_msg = ""
-
- # Get open positions from unified trades table (now including any newly synced ones)
- open_positions = stats.get_open_positions()
-
- positions_text = f"๐ <b>Open Positions</b>\n\n{sync_msg}"
-
- if open_positions:
- total_unrealized = 0
- total_position_value = 0
-
- # Also get fresh exchange data for display
- fresh_exchange_positions = self.trading_engine.get_positions() or []
- exchange_data_map = {pos.get('symbol'): pos for pos in fresh_exchange_positions}
-
- for position_trade in open_positions:
- symbol = position_trade['symbol']
- token = symbol.split('/')[0] if '/' in symbol else symbol
- position_side = position_trade['position_side'] # 'long' or 'short'
- entry_price = position_trade['entry_price']
- current_amount = position_trade['current_position_size']
- trade_type = position_trade.get('trade_type', 'manual')
-
- # ๐ Use fresh exchange data if available (most accurate)
- exchange_pos = exchange_data_map.get(symbol)
- if exchange_pos:
- # Use exchange's official data
- unrealized_pnl = float(exchange_pos.get('unrealizedPnl', 0))
- mark_price = float(exchange_pos.get('markPrice') or 0)
- position_value = float(exchange_pos.get('notional', 0))
- liquidation_price = float(exchange_pos.get('liquidationPrice', 0))
- margin_used = float(exchange_pos.get('initialMargin', 0))
- leverage = float(exchange_pos.get('leverage', 1))
- pnl_percentage = float(exchange_pos.get('percentage', 0))
-
- # Get mark price from market data if not in position data
- if mark_price <= 0:
- try:
- market_data = self.trading_engine.get_market_data(symbol)
- if market_data and market_data.get('ticker'):
- mark_price = float(market_data['ticker'].get('last', entry_price))
- except:
- mark_price = entry_price # Fallback
- else:
- # Fallback to our calculation if exchange data unavailable
- unrealized_pnl = position_trade.get('unrealized_pnl', 0)
- mark_price = entry_price # Fallback
- try:
- market_data = self.trading_engine.get_market_data(symbol)
- if market_data and market_data.get('ticker'):
- mark_price = float(market_data['ticker'].get('last', entry_price))
-
- # Calculate unrealized PnL with current price
- if position_side == 'long':
- unrealized_pnl = current_amount * (mark_price - entry_price)
- else: # Short position
- unrealized_pnl = current_amount * (entry_price - mark_price)
- except:
- pass # Use entry price as fallback
-
- position_value = abs(current_amount) * mark_price
- liquidation_price = None
- margin_used = None
- leverage = None
- pnl_percentage = (unrealized_pnl / position_value * 100) if position_value > 0 else 0
-
- total_position_value += position_value
- total_unrealized += unrealized_pnl
-
- # Position emoji and formatting
- if position_side == 'long':
- pos_emoji = "๐ข"
- direction = "LONG"
- else: # Short position
- pos_emoji = "๐ด"
- direction = "SHORT"
-
- pnl_emoji = "๐ข" if unrealized_pnl >= 0 else "๐ด"
-
- # Format prices with proper precision for this token
- formatter = get_formatter()
- entry_price_str = formatter.format_price_with_symbol(entry_price, token)
- mark_price_str = formatter.format_price_with_symbol(mark_price, token)
-
- # Trade type indicator
- type_indicator = ""
- if trade_type == 'external':
- type_indicator = " ๐" # External/synced position
- elif trade_type == 'bot':
- type_indicator = " ๐ค" # Bot-created position
-
- positions_text += f"{pos_emoji} <b>{token} ({direction}){type_indicator}</b>\n"
- positions_text += f" ๐ Size: {abs(current_amount):.6f} {token}\n"
- positions_text += f" ๐ฐ Entry: {entry_price_str}\n"
- positions_text += f" ๐ Mark: {mark_price_str}\n"
- positions_text += f" ๐ต Value: ${position_value:,.2f}\n"
- positions_text += f" {pnl_emoji} P&L: ${unrealized_pnl:,.2f} ({pnl_percentage:+.2f}%)\n"
-
- # Show exchange-provided risk data if available
- if leverage:
- positions_text += f" โก Leverage: {leverage:.1f}x\n"
- if margin_used:
- positions_text += f" ๐ณ Margin: ${margin_used:,.2f}\n"
- if liquidation_price:
- liq_price_str = formatter.format_price_with_symbol(liquidation_price, token)
- positions_text += f" โ ๏ธ Liquidation: {liq_price_str}\n"
-
- # Show stop loss if linked
- if position_trade.get('stop_loss_price'):
- sl_price = position_trade['stop_loss_price']
- sl_status = "Pending" if not position_trade.get('stop_loss_order_id') else "Active"
- positions_text += f" ๐ Stop Loss: {formatter.format_price_with_symbol(sl_price, token)} ({sl_status})\n"
-
- # Show take profit if linked
- if position_trade.get('take_profit_price'):
- tp_price = position_trade['take_profit_price']
- tp_status = "Pending" if not position_trade.get('take_profit_order_id') else "Active"
- positions_text += f" ๐ฏ Take Profit: {formatter.format_price_with_symbol(tp_price, token)} ({tp_status})\n"
-
- positions_text += f" ๐ Lifecycle ID: {position_trade['trade_lifecycle_id'][:8]}\n\n"
-
- # Portfolio summary
- portfolio_emoji = "๐ข" if total_unrealized >= 0 else "๐ด"
- positions_text += f"๐ผ <b>Total Portfolio:</b>\n"
- positions_text += f" ๐ต Total Value: ${total_position_value:,.2f}\n"
- positions_text += f" {portfolio_emoji} Total P&L: ${total_unrealized:,.2f}\n\n"
- positions_text += f"๐ค <b>Legend:</b> ๐ค Bot-created โข ๐ External/synced\n"
- positions_text += f"๐ก Use /sl [token] [price] or /tp [token] [price] to set risk management"
-
- else:
- positions_text += "๐ญ No open positions\n\n"
- positions_text += "๐ก Use /long or /short to open a position"
-
- await context.bot.send_message(chat_id=chat_id, text=positions_text, parse_mode='HTML')
-
- async def orders_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /orders command."""
- chat_id = update.effective_chat.id
- if not self._is_authorized(chat_id):
- await context.bot.send_message(chat_id=chat_id, text="โ Unauthorized access.")
- return
-
- orders = self.trading_engine.get_orders()
-
- if orders is not None:
- if len(orders) > 0:
- orders_text = "๐ <b>Open Orders</b>\n\n"
-
- # Group orders by symbol
- orders_by_symbol = {}
- for order in orders:
- symbol = order.get('symbol', '').replace('/USDC:USDC', '')
- if symbol not in orders_by_symbol:
- orders_by_symbol[symbol] = []
- orders_by_symbol[symbol].append(order)
-
- for symbol, symbol_orders in orders_by_symbol.items():
- orders_text += f"๐ <b>{symbol}</b>\n"
-
- formatter = get_formatter()
- for order in symbol_orders:
- side = order.get('side', '').upper()
- amount = float(order.get('amount', 0))
- price = float(order.get('price', 0))
- order_type = order.get('type', 'unknown').title()
- order_id = order.get('id', 'N/A')
-
- # Order emoji
- side_emoji = "๐ข" if side == "BUY" else "๐ด"
-
- orders_text += f" {side_emoji} {side} {amount:.6f} @ {formatter.format_price_with_symbol(price, symbol)}\n"
- orders_text += f" ๐ Type: {order_type} | ID: {order_id}\n"
-
- # Check for pending stop losses linked to this order
- stats = self.trading_engine.get_stats()
- if stats:
- # Try to find this order in our database to get its bot_order_ref_id
- order_in_db = stats.get_order_by_exchange_id(order_id)
- if order_in_db:
- bot_ref_id = order_in_db.get('bot_order_ref_id')
- if bot_ref_id:
- # Look for pending stop losses with this order as parent
- pending_sls = stats.get_orders_by_status(
- status='pending_trigger',
- order_type_filter='stop_limit_trigger',
- parent_bot_order_ref_id=bot_ref_id
- )
-
- if pending_sls:
- sl_order = pending_sls[0] # Should only be one
- sl_price = sl_order.get('price', 0)
- sl_side = sl_order.get('side', '').upper()
- orders_text += f" ๐ Pending SL: {sl_side} @ {formatter.format_price_with_symbol(sl_price, symbol)} (activates when filled)\n"
-
- orders_text += "\n"
-
- orders_text += f"๐ผ <b>Total Orders:</b> {len(orders)}\n"
- orders_text += f"๐ก Use /coo [token] to cancel orders"
-
- else:
- orders_text = "๐ <b>Open Orders</b>\n\n"
- orders_text += "๐ญ No open orders\n\n"
- orders_text += "๐ก Use /long, /short, /sl, or /tp to create orders"
-
- await context.bot.send_message(chat_id=chat_id, text=orders_text, parse_mode='HTML')
- else:
- await context.bot.send_message(chat_id=chat_id, text="โ Could not fetch orders")
-
- async def stats_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /stats command."""
- chat_id = update.effective_chat.id
- if not self._is_authorized(chat_id):
- await context.bot.send_message(chat_id=chat_id, text="โ Unauthorized access.")
- return
-
- # Get current balance for stats
- balance = self.trading_engine.get_balance()
- current_balance = 0
- if balance and balance.get('total'):
- current_balance = float(balance['total'].get('USDC', 0))
-
- stats = self.trading_engine.get_stats()
- if stats:
- stats_message = stats.format_stats_message(current_balance)
- await context.bot.send_message(chat_id=chat_id, text=stats_message, parse_mode='HTML')
- else:
- await context.bot.send_message(chat_id=chat_id, text="โ Could not load trading statistics")
-
- async def trades_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /trades command - Show recent trade history."""
- if not self._is_authorized(update):
- return
-
- try:
- stats = self.trading_engine.get_stats()
- if not stats:
- await update.message.reply_text("โ Trading statistics not available.", parse_mode='HTML')
- return
-
- # Get recent trades (limit to last 20)
- recent_trades = stats.get_recent_trades(limit=20)
-
- if not recent_trades:
- await update.message.reply_text("๐ <b>No trades found.</b>", parse_mode='HTML')
- return
-
- message = "๐ <b>Recent Trades (Last 20)</b>\n\n"
-
- for trade in recent_trades:
- symbol = trade['symbol']
- token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0] if ':' in symbol else symbol
- side = trade['side'].upper()
- amount = trade['amount']
- price = trade['price']
- timestamp = trade['timestamp']
- pnl = trade.get('realized_pnl', 0)
-
- # Format timestamp
- try:
- from datetime import datetime
- dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
- time_str = dt.strftime('%m/%d %H:%M')
- except:
- time_str = "Unknown"
-
- # PnL emoji and formatting
- if pnl > 0:
- pnl_emoji = "๐ข"
- pnl_str = f"+${pnl:.2f}"
- elif pnl < 0:
- pnl_emoji = "๐ด"
- pnl_str = f"${pnl:.2f}"
- else:
- pnl_emoji = "โช"
- pnl_str = "$0.00"
-
- side_emoji = "๐ข" if side == 'BUY' else "๐ด"
-
- message += f"{side_emoji} <b>{side}</b> {amount} {token} @ ${price:,.2f}\n"
- message += f" {pnl_emoji} P&L: {pnl_str} | {time_str}\n\n"
-
- await update.message.reply_text(message, parse_mode='HTML')
-
- except Exception as e:
- logger.error(f"Error in trades command: {e}")
- await update.message.reply_text("โ Error retrieving trade history.", parse_mode='HTML')
-
- async def cycles_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /cycles command - Show trade cycles and lifecycle statistics."""
- if not self._is_authorized(update):
- return
-
- try:
- stats = self.trading_engine.get_stats()
- if not stats:
- await update.message.reply_text("โ Trading statistics not available.", parse_mode='HTML')
- return
-
- # Get trade cycle performance stats
- cycle_stats = stats.get_trade_cycle_performance_stats()
-
- if not cycle_stats or cycle_stats.get('total_closed_trades', 0) == 0:
- await update.message.reply_text("๐ <b>No completed trade cycles found.</b>", parse_mode='HTML')
- return
-
- # Get recent trade cycles
- recent_cycles = stats.get_recent_trade_cycles(limit=10)
- open_cycles = stats.get_open_trade_cycles()
-
- message = "๐ <b>Trade Cycle Statistics</b>\n\n"
-
- # Performance summary
- total_trades = cycle_stats.get('total_closed_trades', 0)
- win_rate = cycle_stats.get('win_rate', 0)
- total_pnl = cycle_stats.get('total_pnl', 0)
- avg_duration = cycle_stats.get('avg_duration_minutes', 0)
- profit_factor = cycle_stats.get('profit_factor', 0)
- stop_loss_rate = cycle_stats.get('stop_loss_rate', 0)
-
- pnl_emoji = "๐ข" if total_pnl >= 0 else "๐ด"
-
- message += f"๐ <b>Performance Summary:</b>\n"
- message += f"โข Total Completed: {total_trades} trades\n"
- message += f"โข Win Rate: {win_rate:.1f}%\n"
- message += f"โข {pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
- message += f"โข Avg Duration: {avg_duration:.1f} min\n"
- message += f"โข Profit Factor: {profit_factor:.2f}\n"
- message += f"โข Stop Loss Rate: {stop_loss_rate:.1f}%\n\n"
-
- # Open cycles
- if open_cycles:
- message += f"๐ข <b>Open Cycles ({len(open_cycles)}):</b>\n"
- for cycle in open_cycles[:5]: # Show max 5
- symbol = cycle['symbol']
- token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
- side = cycle['side'].upper()
- entry_price = cycle.get('entry_price', 0)
- side_emoji = "๐" if side == 'BUY' else "๐"
- message += f"{side_emoji} {side} {token} @ ${entry_price:.2f}\n"
- message += "\n"
-
- # Recent completed cycles
- if recent_cycles:
- completed_recent = [c for c in recent_cycles if c['status'] == 'closed'][:5]
- if completed_recent:
- message += f"๐ <b>Recent Completed ({len(completed_recent)}):</b>\n"
- for cycle in completed_recent:
- symbol = cycle['symbol']
- token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
- side = cycle['side'].upper()
- entry_price = cycle.get('entry_price', 0)
- exit_price = cycle.get('exit_price', 0)
- pnl = cycle.get('realized_pnl', 0)
- exit_type = cycle.get('exit_type', 'unknown')
- duration = cycle.get('duration_seconds', 0)
-
- # Format duration
- if duration > 3600:
- duration_str = f"{duration//3600:.0f}h"
- elif duration > 60:
- duration_str = f"{duration//60:.0f}m"
- else:
- duration_str = f"{duration}s"
-
- pnl_emoji = "๐ข" if pnl >= 0 else "๐ด"
- side_emoji = "๐" if side == 'BUY' else "๐"
- exit_emoji = "๐" if exit_type == 'stop_loss' else "๐ฏ" if exit_type == 'take_profit' else "๐"
-
- message += f"{side_emoji} {side} {token}: ${entry_price:.2f} โ ${exit_price:.2f}\n"
- message += f" {pnl_emoji} ${pnl:+.2f} | {exit_emoji} {exit_type} | {duration_str}\n"
- message += "\n"
-
- message += "๐ก Trade cycles track complete trades from open to close with full P&L analysis."
-
- await update.message.reply_text(message, parse_mode='HTML')
-
- except Exception as e:
- error_message = f"โ Error processing cycles command: {str(e)}"
- await context.bot.send_message(chat_id=chat_id, text=error_message)
- logger.error(f"Error in cycles command: {e}")
-
- async def active_trades_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /active command to show active trades (Phase 1 testing)."""
- chat_id = update.effective_chat.id
- if not self._is_authorized(chat_id):
- await context.bot.send_message(chat_id=chat_id, text="โ Unauthorized access.")
- return
-
- try:
- stats = self.trading_engine.get_stats()
- if not stats:
- await context.bot.send_message(chat_id=chat_id, text="โ Could not load trading statistics")
- return
-
- # Get all active trades
- all_active_trades = stats.get_all_active_trades()
-
- if not all_active_trades:
- await context.bot.send_message(
- chat_id=chat_id,
- text="๐ <b>Active Trades (Phase 1)</b>\n\n๐ญ No active trades found.",
- parse_mode='HTML'
- )
- return
-
- # Group by status
- active_trades_by_status = {}
- for trade in all_active_trades:
- status = trade['status']
- if status not in active_trades_by_status:
- active_trades_by_status[status] = []
- active_trades_by_status[status].append(trade)
-
- message_text = "๐ <b>Active Trades (Phase 1)</b>\n\n"
-
- # Show each status group
- for status, trades in active_trades_by_status.items():
- status_emoji = {
- 'pending': 'โณ',
- 'active': '๐ข',
- 'closed': 'โ
',
- 'cancelled': 'โ'
- }.get(status, '๐')
-
- message_text += f"{status_emoji} <b>{status.upper()}</b> ({len(trades)} trades):\n"
-
- for trade in trades[:5]: # Limit to 5 per status to avoid long messages
- symbol = trade['symbol']
- token = symbol.split('/')[0] if '/' in symbol else symbol
- side = trade['side'].upper()
- entry_price = trade.get('entry_price')
- entry_amount = trade.get('entry_amount')
- realized_pnl = trade.get('realized_pnl', 0)
-
- message_text += f" โข {side} {token}"
-
- if entry_price and entry_amount:
- message_text += f" | {entry_amount:.6f} @ ${entry_price:.2f}"
-
- if status == 'closed' and realized_pnl != 0:
- pnl_emoji = "๐ข" if realized_pnl >= 0 else "๐ด"
- message_text += f" | {pnl_emoji} ${realized_pnl:.2f}"
-
- if trade.get('stop_loss_price'):
- message_text += f" | SL: ${trade['stop_loss_price']:.2f}"
-
- message_text += "\n"
-
- if len(trades) > 5:
- message_text += f" ... and {len(trades) - 5} more\n"
-
- message_text += "\n"
-
- # Add summary
- total_trades = len(all_active_trades)
- pending_count = len(active_trades_by_status.get('pending', []))
- active_count = len(active_trades_by_status.get('active', []))
- closed_count = len(active_trades_by_status.get('closed', []))
- cancelled_count = len(active_trades_by_status.get('cancelled', []))
-
- message_text += f"๐ <b>Summary:</b>\n"
- message_text += f" Total: {total_trades} | "
- message_text += f"Pending: {pending_count} | "
- message_text += f"Active: {active_count} | "
- message_text += f"Closed: {closed_count} | "
- message_text += f"Cancelled: {cancelled_count}\n\n"
-
- message_text += f"๐ก This is Phase 1 testing - active trades run parallel to trade cycles"
-
- await context.bot.send_message(chat_id=chat_id, text=message_text.strip(), parse_mode='HTML')
-
- except Exception as e:
- error_message = f"โ Error processing active trades command: {str(e)}"
- await context.bot.send_message(chat_id=chat_id, text=error_message)
- logger.error(f"Error in active trades command: {e}")
-
- async def market_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /market command."""
- chat_id = update.effective_chat.id
- if not self._is_authorized(chat_id):
- await context.bot.send_message(chat_id=chat_id, text="โ Unauthorized access.")
- return
-
- # Get token from arguments or use default
- if context.args and len(context.args) > 0:
- token = context.args[0].upper()
- else:
- token = Config.DEFAULT_TRADING_TOKEN
-
- symbol = f"{token}/USDC:USDC"
- market_data = self.trading_engine.get_market_data(symbol)
-
- if market_data:
- ticker = market_data.get('ticker', {})
-
- current_price = float(ticker.get('last', 0.0) or 0.0)
- bid_price = float(ticker.get('bid', 0.0) or 0.0)
- ask_price = float(ticker.get('ask', 0.0) or 0.0)
- raw_base_volume = ticker.get('baseVolume')
- volume_24h = float(raw_base_volume if raw_base_volume is not None else 0.0)
- raw_change_24h = ticker.get('change')
- change_24h = float(raw_change_24h if raw_change_24h is not None else 0.0)
- raw_percentage = ticker.get('percentage')
- change_percent = float(raw_percentage if raw_percentage is not None else 0.0)
- high_24h = float(ticker.get('high', 0.0) or 0.0)
- low_24h = float(ticker.get('low', 0.0) or 0.0)
-
- # Market direction emoji
- trend_emoji = "๐ข" if change_24h >= 0 else "๐ด"
-
- # Format prices with proper precision for this token
- formatter = get_formatter()
- current_price_str = formatter.format_price_with_symbol(current_price, token)
- bid_price_str = formatter.format_price_with_symbol(bid_price, token)
- ask_price_str = formatter.format_price_with_symbol(ask_price, token)
- spread_str = formatter.format_price_with_symbol(ask_price - bid_price, token)
- high_24h_str = formatter.format_price_with_symbol(high_24h, token)
- low_24h_str = formatter.format_price_with_symbol(low_24h, token)
- change_24h_str = formatter.format_price_with_symbol(change_24h, token)
-
- market_text = f"""
- ๐ <b>{token} Market Data</b>
- ๐ฐ <b>Price Information:</b>
- ๐ต Current: {current_price_str}
- ๐ข Bid: {bid_price_str}
- ๐ด Ask: {ask_price_str}
- ๐ Spread: {spread_str}
- ๐ <b>24h Statistics:</b>
- {trend_emoji} Change: {change_24h_str} ({change_percent:+.2f}%)
- ๐ High: {high_24h_str}
- ๐ป Low: {low_24h_str}
- ๐ Volume: {volume_24h:,.2f} {token}
- โฐ <b>Last Updated:</b> {datetime.now().strftime('%H:%M:%S')}
- """
-
- await context.bot.send_message(chat_id=chat_id, text=market_text.strip(), parse_mode='HTML')
- else:
- await context.bot.send_message(chat_id=chat_id, text=f"โ Could not fetch market data for {token}")
-
- async def price_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /price command."""
- chat_id = update.effective_chat.id
- if not self._is_authorized(chat_id):
- await context.bot.send_message(chat_id=chat_id, text="โ Unauthorized access.")
- return
-
- # Get token from arguments or use default
- if context.args and len(context.args) > 0:
- token = context.args[0].upper()
- else:
- token = Config.DEFAULT_TRADING_TOKEN
-
- symbol = f"{token}/USDC:USDC"
- market_data = self.trading_engine.get_market_data(symbol)
-
- if market_data:
- ticker = market_data.get('ticker', {})
- current_price = float(ticker.get('last', 0.0) or 0.0)
- raw_change_24h = ticker.get('change')
- change_24h = float(raw_change_24h if raw_change_24h is not None else 0.0)
- raw_percentage = ticker.get('percentage')
- change_percent = float(raw_percentage if raw_percentage is not None else 0.0)
-
- # Price direction emoji
- trend_emoji = "๐ข" if change_24h >= 0 else "๐ด"
-
- # Format prices with proper precision for this token
- formatter = get_formatter()
- current_price_str = formatter.format_price_with_symbol(current_price, token)
- change_24h_str = formatter.format_price_with_symbol(change_24h, token)
-
- price_text = f"""
- ๐ต <b>{token} Price</b>
- ๐ฐ {current_price_str}
- {trend_emoji} {change_percent:+.2f}% ({change_24h_str})
- โฐ {datetime.now().strftime('%H:%M:%S')}
- """
-
- await context.bot.send_message(chat_id=chat_id, text=price_text.strip(), parse_mode='HTML')
- else:
- await context.bot.send_message(chat_id=chat_id, text=f"โ Could not fetch price for {token}")
-
- async def performance_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /performance command to show token performance ranking or detailed stats."""
- chat_id = update.effective_chat.id
- if not self._is_authorized(chat_id):
- await context.bot.send_message(chat_id=chat_id, text="โ Unauthorized access.")
- return
-
- try:
- # Check if specific token is requested
- if context.args and len(context.args) >= 1:
- # Detailed performance for specific token
- token = context.args[0].upper()
- await self._show_token_performance(chat_id, token, context)
- else:
- # Show token performance ranking
- await self._show_performance_ranking(chat_id, context)
-
- except Exception as e:
- error_message = f"โ Error processing performance command: {str(e)}"
- await context.bot.send_message(chat_id=chat_id, text=error_message)
- logger.error(f"Error in performance command: {e}")
- async def _show_performance_ranking(self, chat_id: str, context: ContextTypes.DEFAULT_TYPE):
- """Show token performance ranking (compressed view)."""
- stats = self.trading_engine.get_stats()
- if not stats:
- await context.bot.send_message(chat_id=chat_id, text="โ Could not load trading statistics")
- return
-
- token_performance = stats.get_token_performance()
-
- if not token_performance:
- await context.bot.send_message(chat_id=chat_id, text=
- "๐ <b>Token Performance</b>\n\n"
- "๐ญ No trading data available yet.\n\n"
- "๐ก Performance tracking starts after your first completed trades.\n"
- "Use /long or /short to start trading!",
- parse_mode='HTML'
- )
- return
-
- # Sort tokens by total P&L (best to worst)
- sorted_tokens = sorted(
- token_performance.items(),
- key=lambda x: x[1]['total_pnl'],
- reverse=True
- )
-
- performance_text = "๐ <b>Token Performance Ranking</b>\n\n"
-
- # Add ranking with emojis
- for i, (token, stats_data) in enumerate(sorted_tokens, 1):
- # Ranking emoji
- if i == 1:
- rank_emoji = "๐ฅ"
- elif i == 2:
- rank_emoji = "๐ฅ"
- elif i == 3:
- rank_emoji = "๐ฅ"
- else:
- rank_emoji = f"#{i}"
-
- # P&L emoji
- pnl_emoji = "๐ข" if stats_data['total_pnl'] >= 0 else "๐ด"
-
- # Format the line
- performance_text += f"{rank_emoji} <b>{token}</b>\n"
- performance_text += f" {pnl_emoji} P&L: ${stats_data['total_pnl']:,.2f} ({stats_data['pnl_percentage']:+.1f}%)\n"
- performance_text += f" ๐ Trades: {stats_data['completed_trades']}"
-
- # Add win rate if there are completed trades
- if stats_data['completed_trades'] > 0:
- performance_text += f" | Win: {stats_data['win_rate']:.0f}%"
-
- performance_text += "\n\n"
-
- # Add summary
- total_pnl = sum(stats_data['total_pnl'] for stats_data in token_performance.values())
- total_trades = sum(stats_data['completed_trades'] for stats_data in token_performance.values())
- total_pnl_emoji = "๐ข" if total_pnl >= 0 else "๐ด"
-
- performance_text += f"๐ผ <b>Portfolio Summary:</b>\n"
- performance_text += f" {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
- performance_text += f" ๐ Tokens Traded: {len(token_performance)}\n"
- performance_text += f" ๐ Completed Trades: {total_trades}\n\n"
-
- performance_text += f"๐ก <b>Usage:</b> <code>/performance BTC</code> for detailed {Config.DEFAULT_TRADING_TOKEN} stats"
-
- await context.bot.send_message(chat_id=chat_id, text=performance_text.strip(), parse_mode='HTML')
- async def _show_token_performance(self, chat_id: str, token: str, context: ContextTypes.DEFAULT_TYPE):
- """Show detailed performance for a specific token."""
- stats = self.trading_engine.get_stats()
- if not stats:
- await context.bot.send_message(chat_id=chat_id, text="โ Could not load trading statistics")
- return
-
- token_stats = stats.get_token_detailed_stats(token)
-
- # Check if token has any data
- if token_stats.get('total_trades', 0) == 0:
- await context.bot.send_message(chat_id=chat_id, text=
- f"๐ <b>{token} Performance</b>\n\n"
- f"๐ญ No trading history found for {token}.\n\n"
- f"๐ก Start trading {token} with:\n"
- f"โข <code>/long {token} 100</code>\n"
- f"โข <code>/short {token} 100</code>\n\n"
- f"๐ Use <code>/performance</code> to see all token rankings.",
- parse_mode='HTML'
- )
- return
-
- # Check if there's a message (no completed trades)
- if 'message' in token_stats and token_stats.get('completed_trades', 0) == 0:
- await context.bot.send_message(chat_id=chat_id, text=
- f"๐ <b>{token} Performance</b>\n\n"
- f"{token_stats['message']}\n\n"
- f"๐ <b>Current Activity:</b>\n"
- f"โข Total Trades: {token_stats['total_trades']}\n"
- f"โข Buy Orders: {token_stats.get('buy_trades', 0)}\n"
- f"โข Sell Orders: {token_stats.get('sell_trades', 0)}\n"
- f"โข Volume: ${token_stats.get('total_volume', 0):,.2f}\n\n"
- f"๐ก Complete some trades to see P&L statistics!\n"
- f"๐ Use <code>/performance</code> to see all token rankings.",
- parse_mode='HTML'
- )
- return
-
- # Detailed stats display
- pnl_emoji = "๐ข" if token_stats['total_pnl'] >= 0 else "๐ด"
-
- performance_text = f"""
- ๐ <b>{token} Detailed Performance</b>
- ๐ฐ <b>P&L Summary:</b>
- โข {pnl_emoji} Total P&L: ${token_stats['total_pnl']:,.2f} ({token_stats['pnl_percentage']:+.2f}%)
- โข ๐ต Total Volume: ${token_stats['completed_volume']:,.2f}
- โข ๐ Expectancy: ${token_stats['expectancy']:,.2f}
- ๐ <b>Trading Activity:</b>
- โข Total Trades: {token_stats['total_trades']}
- โข Completed: {token_stats['completed_trades']}
- โข Buy Orders: {token_stats['buy_trades']}
- โข Sell Orders: {token_stats['sell_trades']}
- ๐ <b>Performance Metrics:</b>
- โข Win Rate: {token_stats['win_rate']:.1f}%
- โข Profit Factor: {token_stats['profit_factor']:.2f}
- โข Wins: {token_stats['total_wins']} | Losses: {token_stats['total_losses']}
- ๐ก <b>Best/Worst:</b>
- โข Largest Win: ${token_stats['largest_win']:,.2f}
- โข Largest Loss: ${token_stats['largest_loss']:,.2f}
- โข Avg Win: ${token_stats['avg_win']:,.2f}
- โข Avg Loss: ${token_stats['avg_loss']:,.2f}
- """
-
- # Add recent trades if available
- if token_stats.get('recent_trades'):
- performance_text += f"\n๐ <b>Recent Trades:</b>\n"
- for trade in token_stats['recent_trades'][-3:]: # Last 3 trades
- trade_time = datetime.fromisoformat(trade['timestamp']).strftime('%m/%d %H:%M')
- side_emoji = "๐ข" if trade['side'] == 'buy' else "๐ด"
- pnl_display = f" | P&L: ${trade.get('pnl', 0):.2f}" if trade.get('pnl', 0) != 0 else ""
-
- performance_text += f"โข {side_emoji} {trade['side'].upper()} ${trade['value']:,.0f} @ {trade_time}{pnl_display}\n"
-
- performance_text += f"\n๐ Use <code>/performance</code> to see all token rankings"
-
- await context.bot.send_message(chat_id=chat_id, text=performance_text.strip(), parse_mode='HTML')
- async def daily_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /daily command to show daily performance stats."""
- chat_id = update.effective_chat.id
- if not self._is_authorized(chat_id):
- await context.bot.send_message(chat_id=chat_id, text="โ Unauthorized access.")
- return
-
- try:
- stats = self.trading_engine.get_stats()
- if not stats:
- await context.bot.send_message(chat_id=chat_id, text="โ Could not load trading statistics")
- return
-
- daily_stats = stats.get_daily_stats(10)
-
- if not daily_stats:
- await context.bot.send_message(chat_id=chat_id, text=
- "๐
<b>Daily Performance</b>\n\n"
- "๐ญ No daily performance data available yet.\n\n"
- "๐ก Daily stats are calculated from completed trades.\n"
- "Start trading to see daily performance!",
- parse_mode='HTML'
- )
- return
-
- daily_text = "๐
<b>Daily Performance (Last 10 Days)</b>\n\n"
-
- total_pnl = 0
- total_trades = 0
- trading_days = 0
-
- for day_stats in daily_stats:
- if day_stats['has_trades']:
- # Day with completed trades
- pnl_emoji = "๐ข" if day_stats['pnl'] >= 0 else "๐ด"
- daily_text += f"๐ <b>{day_stats['date_formatted']}</b>\n"
- daily_text += f" {pnl_emoji} P&L: ${day_stats['pnl']:,.2f} ({day_stats['pnl_pct']:+.1f}%)\n"
- daily_text += f" ๐ Trades: {day_stats['trades']}\n\n"
-
- total_pnl += day_stats['pnl']
- total_trades += day_stats['trades']
- trading_days += 1
- else:
- # Day with no trades
- daily_text += f"๐ <b>{day_stats['date_formatted']}</b>\n"
- daily_text += f" ๐ญ No trading activity\n\n"
-
- # Add summary
- if trading_days > 0:
- avg_daily_pnl = total_pnl / trading_days
- avg_pnl_emoji = "๐ข" if avg_daily_pnl >= 0 else "๐ด"
-
- daily_text += f"๐ <b>Period Summary:</b>\n"
- daily_text += f" {avg_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
- daily_text += f" ๐ Trading Days: {trading_days}/10\n"
- daily_text += f" ๐ Avg Daily P&L: ${avg_daily_pnl:,.2f}\n"
- daily_text += f" ๐ Total Trades: {total_trades}\n"
-
- await context.bot.send_message(chat_id=chat_id, text=daily_text.strip(), parse_mode='HTML')
-
- except Exception as e:
- error_message = f"โ Error processing daily command: {str(e)}"
- await context.bot.send_message(chat_id=chat_id, text=error_message)
- logger.error(f"Error in daily command: {e}")
-
- async def weekly_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /weekly command to show weekly performance stats."""
- chat_id = update.effective_chat.id
- if not self._is_authorized(chat_id):
- await context.bot.send_message(chat_id=chat_id, text="โ Unauthorized access.")
- return
-
- try:
- stats = self.trading_engine.get_stats()
- if not stats:
- await context.bot.send_message(chat_id=chat_id, text="โ Could not load trading statistics")
- return
-
- weekly_stats = stats.get_weekly_stats(10)
-
- if not weekly_stats:
- await context.bot.send_message(chat_id=chat_id, text=
- "๐ <b>Weekly Performance</b>\n\n"
- "๐ญ No weekly performance data available yet.\n\n"
- "๐ก Weekly stats are calculated from completed trades.\n"
- "Start trading to see weekly performance!",
- parse_mode='HTML'
- )
- return
-
- weekly_text = "๐ <b>Weekly Performance (Last 10 Weeks)</b>\n\n"
-
- total_pnl = 0
- total_trades = 0
- trading_weeks = 0
-
- for week_stats in weekly_stats:
- if week_stats['has_trades']:
- # Week with completed trades
- pnl_emoji = "๐ข" if week_stats['pnl'] >= 0 else "๐ด"
- weekly_text += f"๐ <b>{week_stats['week_formatted']}</b>\n"
- weekly_text += f" {pnl_emoji} P&L: ${week_stats['pnl']:,.2f} ({week_stats['pnl_pct']:+.1f}%)\n"
- weekly_text += f" ๐ Trades: {week_stats['trades']}\n\n"
-
- total_pnl += week_stats['pnl']
- total_trades += week_stats['trades']
- trading_weeks += 1
- else:
- # Week with no trades
- weekly_text += f"๐ <b>{week_stats['week_formatted']}</b>\n"
- weekly_text += f" ๐ญ No completed trades\n\n"
-
- # Add summary
- if trading_weeks > 0:
- total_pnl_emoji = "๐ข" if total_pnl >= 0 else "๐ด"
- weekly_text += f"๐ผ <b>10-Week Summary:</b>\n"
- weekly_text += f" {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
- weekly_text += f" ๐ Total Trades: {total_trades}\n"
- weekly_text += f" ๐ Trading Weeks: {trading_weeks}/10\n"
- weekly_text += f" ๐ Avg per Trading Week: ${total_pnl/trading_weeks:,.2f}"
- else:
- weekly_text += f"๐ผ <b>10-Week Summary:</b>\n"
- weekly_text += f" ๐ญ No completed trades in the last 10 weeks\n"
- weekly_text += f" ๐ก Start trading to see weekly performance!"
-
- await context.bot.send_message(chat_id=chat_id, text=weekly_text.strip(), parse_mode='HTML')
-
- except Exception as e:
- error_message = f"โ Error processing weekly command: {str(e)}"
- await context.bot.send_message(chat_id=chat_id, text=error_message)
- logger.error(f"Error in weekly command: {e}")
- async def monthly_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /monthly command to show monthly performance stats."""
- chat_id = update.effective_chat.id
- if not self._is_authorized(chat_id):
- await context.bot.send_message(chat_id=chat_id, text="โ Unauthorized access.")
- return
-
- try:
- stats = self.trading_engine.get_stats()
- if not stats:
- await context.bot.send_message(chat_id=chat_id, text="โ Could not load trading statistics")
- return
-
- monthly_stats = stats.get_monthly_stats(10)
-
- if not monthly_stats:
- await context.bot.send_message(chat_id=chat_id, text=
- "๐ <b>Monthly Performance</b>\n\n"
- "๐ญ No monthly performance data available yet.\n\n"
- "๐ก Monthly stats are calculated from completed trades.\n"
- "Start trading to see monthly performance!",
- parse_mode='HTML'
- )
- return
-
- monthly_text = "๐ <b>Monthly Performance (Last 10 Months)</b>\n\n"
-
- total_pnl = 0
- total_trades = 0
- trading_months = 0
-
- for month_stats in monthly_stats:
- if month_stats['has_trades']:
- # Month with completed trades
- pnl_emoji = "๐ข" if month_stats['pnl'] >= 0 else "๐ด"
- monthly_text += f"๐
<b>{month_stats['month_formatted']}</b>\n"
- monthly_text += f" {pnl_emoji} P&L: ${month_stats['pnl']:,.2f} ({month_stats['pnl_pct']:+.1f}%)\n"
- monthly_text += f" ๐ Trades: {month_stats['trades']}\n\n"
-
- total_pnl += month_stats['pnl']
- total_trades += month_stats['trades']
- trading_months += 1
- else:
- # Month with no trades
- monthly_text += f"๐
<b>{month_stats['month_formatted']}</b>\n"
- monthly_text += f" ๐ญ No completed trades\n\n"
-
- # Add summary
- if trading_months > 0:
- total_pnl_emoji = "๐ข" if total_pnl >= 0 else "๐ด"
- monthly_text += f"๐ผ <b>10-Month Summary:</b>\n"
- monthly_text += f" {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
- monthly_text += f" ๐ Total Trades: {total_trades}\n"
- monthly_text += f" ๐ Trading Months: {trading_months}/10\n"
- monthly_text += f" ๐ Avg per Trading Month: ${total_pnl/trading_months:,.2f}"
- else:
- monthly_text += f"๐ผ <b>10-Month Summary:</b>\n"
- monthly_text += f" ๐ญ No completed trades in the last 10 months\n"
- monthly_text += f" ๐ก Start trading to see monthly performance!"
-
- await context.bot.send_message(chat_id=chat_id, text=monthly_text.strip(), parse_mode='HTML')
-
- except Exception as e:
- error_message = f"โ Error processing monthly command: {str(e)}"
- await context.bot.send_message(chat_id=chat_id, text=error_message)
- logger.error(f"Error in monthly command: {e}")
-
- async def risk_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /risk command to show advanced risk metrics."""
- chat_id = update.effective_chat.id
- if not self._is_authorized(chat_id):
- await context.bot.send_message(chat_id=chat_id, text="โ Unauthorized access.")
- return
-
- try:
- # Get current balance for context
- balance = self.trading_engine.get_balance()
- current_balance = 0
- if balance and balance.get('total'):
- current_balance = float(balance['total'].get('USDC', 0))
-
- # Get risk metrics and basic stats
- stats = self.trading_engine.get_stats()
- if not stats:
- await context.bot.send_message(chat_id=chat_id, text="โ Could not load trading statistics")
- return
-
- risk_metrics = stats.get_risk_metrics()
- basic_stats = stats.get_basic_stats()
-
- # Check if we have enough data for risk calculations
- if basic_stats['completed_trades'] < 2:
- await context.bot.send_message(chat_id=chat_id, text=
- "๐ <b>Risk Analysis</b>\n\n"
- "๐ญ <b>Insufficient Data</b>\n\n"
- f"โข Current completed trades: {basic_stats['completed_trades']}\n"
- f"โข Required for risk analysis: 2+ trades\n"
- f"โข Daily balance snapshots: {len(stats.data.get('daily_balances', []))}\n\n"
- "๐ก <b>To enable risk analysis:</b>\n"
- "โข Complete more trades to generate returns data\n"
- "โข Bot automatically records daily balance snapshots\n"
- "โข Risk metrics will be available after sufficient trading history\n\n"
- "๐ Use /stats for current performance metrics",
- parse_mode='HTML'
- )
- return
-
- # Format the risk analysis message
- risk_text = f"""
- ๐ <b>Risk Analysis & Advanced Metrics</b>
- ๐ฏ <b>Risk-Adjusted Performance:</b>
- โข Sharpe Ratio: {risk_metrics['sharpe_ratio']:.3f}
- โข Sortino Ratio: {risk_metrics['sortino_ratio']:.3f}
- โข Annual Volatility: {risk_metrics['volatility']:.2f}%
- ๐ <b>Drawdown Analysis:</b>
- โข Maximum Drawdown: {risk_metrics['max_drawdown']:.2f}%
- โข Value at Risk (95%): {risk_metrics['var_95']:.2f}%
- ๐ฐ <b>Portfolio Context:</b>
- โข Current Balance: ${current_balance:,.2f}
- โข Initial Balance: ${basic_stats['initial_balance']:,.2f}
- โข Total P&L: ${basic_stats['total_pnl']:,.2f}
- โข Days Active: {basic_stats['days_active']}
- ๐ <b>Risk Interpretation:</b>
- """
-
- # Add interpretive guidance
- sharpe = risk_metrics['sharpe_ratio']
- if sharpe > 2.0:
- risk_text += "โข ๐ข <b>Excellent</b> risk-adjusted returns (Sharpe > 2.0)\n"
- elif sharpe > 1.0:
- risk_text += "โข ๐ก <b>Good</b> risk-adjusted returns (Sharpe > 1.0)\n"
- elif sharpe > 0.5:
- risk_text += "โข ๐ <b>Moderate</b> risk-adjusted returns (Sharpe > 0.5)\n"
- elif sharpe > 0:
- risk_text += "โข ๐ด <b>Poor</b> risk-adjusted returns (Sharpe > 0)\n"
- else:
- risk_text += "โข โซ <b>Negative</b> risk-adjusted returns (Sharpe < 0)\n"
-
- max_dd = risk_metrics['max_drawdown']
- if max_dd < 5:
- risk_text += "โข ๐ข <b>Low</b> maximum drawdown (< 5%)\n"
- elif max_dd < 15:
- risk_text += "โข ๐ก <b>Moderate</b> maximum drawdown (< 15%)\n"
- elif max_dd < 30:
- risk_text += "โข ๐ <b>High</b> maximum drawdown (< 30%)\n"
- else:
- risk_text += "โข ๐ด <b>Very High</b> maximum drawdown (> 30%)\n"
-
- volatility = risk_metrics['volatility']
- if volatility < 10:
- risk_text += "โข ๐ข <b>Low</b> portfolio volatility (< 10%)\n"
- elif volatility < 25:
- risk_text += "โข ๐ก <b>Moderate</b> portfolio volatility (< 25%)\n"
- elif volatility < 50:
- risk_text += "โข ๐ <b>High</b> portfolio volatility (< 50%)\n"
- else:
- risk_text += "โข ๐ด <b>Very High</b> portfolio volatility (> 50%)\n"
-
- risk_text += f"""
- ๐ก <b>Risk Definitions:</b>
- โข <b>Sharpe Ratio:</b> Risk-adjusted return (excess return / volatility)
- โข <b>Sortino Ratio:</b> Return / downside volatility (focuses on bad volatility)
- โข <b>Max Drawdown:</b> Largest peak-to-trough decline
- โข <b>VaR 95%:</b> Maximum expected loss 95% of the time
- โข <b>Volatility:</b> Annualized standard deviation of returns
- ๐ <b>Data Based On:</b>
- โข Completed Trades: {basic_stats['completed_trades']}
- โข Daily Balance Records: {len(stats.data.get('daily_balances', []))}
- โข Trading Period: {basic_stats['days_active']} days
- ๐ Use /stats for trading performance metrics
- """
-
- await context.bot.send_message(chat_id=chat_id, text=risk_text.strip(), parse_mode='HTML')
-
- except Exception as e:
- error_message = f"โ Error processing risk command: {str(e)}"
- await context.bot.send_message(chat_id=chat_id, text=error_message)
- logger.error(f"Error in risk command: {e}")
-
- async def balance_adjustments_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /balance_adjustments command to show deposit/withdrawal history."""
- chat_id = update.effective_chat.id
- if not self._is_authorized(chat_id):
- await context.bot.send_message(chat_id=chat_id, text="โ Unauthorized access.")
- return
-
- try:
- stats = self.trading_engine.get_stats()
- if not stats:
- await context.bot.send_message(chat_id=chat_id, text="โ Could not load trading statistics")
- return
-
- # Get balance adjustments summary
- adjustments_summary = stats.get_balance_adjustments_summary()
-
- # Get detailed adjustments
- all_adjustments = stats.data.get('balance_adjustments', [])
-
- if not all_adjustments:
- await context.bot.send_message(chat_id=chat_id, text=
- "๐ฐ <b>Balance Adjustments</b>\n\n"
- "๐ญ No deposits or withdrawals detected yet.\n\n"
- "๐ก The bot automatically monitors for deposits and withdrawals\n"
- "every hour to maintain accurate P&L calculations.",
- parse_mode='HTML'
- )
- return
-
- # Format the message
- adjustments_text = f"""
- ๐ฐ <b>Balance Adjustments History</b>
- ๐ <b>Summary:</b>
- โข Total Deposits: ${adjustments_summary['total_deposits']:,.2f}
- โข Total Withdrawals: ${adjustments_summary['total_withdrawals']:,.2f}
- โข Net Adjustment: ${adjustments_summary['net_adjustment']:,.2f}
- โข Total Transactions: {adjustments_summary['adjustment_count']}
- ๐
<b>Recent Adjustments:</b>
- """
-
- # Show last 10 adjustments
- recent_adjustments = sorted(all_adjustments, key=lambda x: x['timestamp'], reverse=True)[:10]
-
- for adj in recent_adjustments:
- try:
- # Format timestamp
- adj_time = datetime.fromisoformat(adj['timestamp']).strftime('%m/%d %H:%M')
-
- # Format type and amount
- if adj['type'] == 'deposit':
- emoji = "๐ฐ"
- amount_str = f"+${adj['amount']:,.2f}"
- else: # withdrawal
- emoji = "๐ธ"
- amount_str = f"-${abs(adj['amount']):,.2f}"
-
- adjustments_text += f"โข {emoji} {adj_time}: {amount_str}\n"
-
- except Exception as adj_error:
- logger.warning(f"Error formatting adjustment: {adj_error}")
- continue
-
- adjustments_text += f"""
- ๐ก <b>How it Works:</b>
- โข Bot checks for deposits/withdrawals every hour
- โข Adjustments maintain accurate P&L calculations
- โข Non-trading balance changes don't affect performance metrics
- โข Trading statistics remain pure and accurate
- โฐ <b>Last Check:</b> {adjustments_summary['last_adjustment'][:16] if adjustments_summary['last_adjustment'] else 'Never'}
- """
-
- await context.bot.send_message(chat_id=chat_id, text=adjustments_text.strip(), parse_mode='HTML')
-
- except Exception as e:
- error_message = f"โ Error processing balance adjustments command: {str(e)}"
- await context.bot.send_message(chat_id=chat_id, text=error_message)
- logger.error(f"Error in balance_adjustments command: {e}")
-
- async def commands_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /commands and /c command with quick action buttons."""
- chat_id = update.effective_chat.id
- if not self._is_authorized(chat_id):
- await context.bot.send_message(chat_id=chat_id, text="โ Unauthorized access.")
- return
-
- commands_text = """
- ๐ฑ <b>Quick Commands</b>
- Tap any button below for instant access to bot functions:
- ๐ก <b>Pro Tip:</b> These buttons work the same as typing the commands manually, but faster!
- """
-
- from telegram import InlineKeyboardButton, InlineKeyboardMarkup
-
- keyboard = [
- [
- InlineKeyboardButton("๐ฐ Balance", callback_data="/balance"),
- InlineKeyboardButton("๐ Positions", callback_data="/positions")
- ],
- [
- InlineKeyboardButton("๐ Orders", callback_data="/orders"),
- InlineKeyboardButton("๐ Stats", callback_data="/stats")
- ],
- [
- InlineKeyboardButton("๐ต Price", callback_data="/price"),
- InlineKeyboardButton("๐ Market", callback_data="/market")
- ],
- [
- InlineKeyboardButton("๐ Performance", callback_data="/performance"),
- InlineKeyboardButton("๐ Alarms", callback_data="/alarm")
- ],
- [
- InlineKeyboardButton("๐
Daily", callback_data="/daily"),
- InlineKeyboardButton("๐ Weekly", callback_data="/weekly")
- ],
- [
- InlineKeyboardButton("๐ Monthly", callback_data="/monthly"),
- InlineKeyboardButton("๐ Trades", callback_data="/trades")
- ],
- [
- InlineKeyboardButton("๐ Monitoring", callback_data="/monitoring"),
- InlineKeyboardButton("๐ Logs", callback_data="/logs")
- ],
- [
- InlineKeyboardButton("โ๏ธ Help", callback_data="/help")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await context.bot.send_message(chat_id=chat_id, text=commands_text, parse_mode='HTML', reply_markup=reply_markup)
- async def _estimate_entry_price_for_orphaned_position(self, symbol: str, contracts: float) -> float:
- """Estimate entry price for an orphaned position by checking recent fills and market data."""
- try:
- # Method 1: Check recent fills from the exchange
- recent_fills = self.trading_engine.get_recent_fills()
- if recent_fills:
- # Look for recent fills for this symbol
- symbol_fills = [fill for fill in recent_fills if fill.get('symbol') == symbol]
-
- if symbol_fills:
- # Get the most recent fill as entry price estimate
- latest_fill = symbol_fills[0] # Assuming sorted by newest first
- fill_price = float(latest_fill.get('price', 0))
-
- if fill_price > 0:
- logger.info(f"๐ก Found recent fill price for {symbol}: ${fill_price:.4f}")
- return fill_price
-
- # Method 2: Use current market price as fallback
- market_data = self.trading_engine.get_market_data(symbol)
- if market_data and market_data.get('ticker'):
- current_price = float(market_data['ticker'].get('last', 0))
-
- if current_price > 0:
- logger.warning(f"โ ๏ธ Using current market price as entry estimate for {symbol}: ${current_price:.4f}")
- return current_price
-
- # Method 3: Last resort - try bid/ask average
- if market_data and market_data.get('ticker'):
- bid = float(market_data['ticker'].get('bid', 0))
- ask = float(market_data['ticker'].get('ask', 0))
-
- if bid > 0 and ask > 0:
- avg_price = (bid + ask) / 2
- logger.warning(f"โ ๏ธ Using bid/ask average as entry estimate for {symbol}: ${avg_price:.4f}")
- return avg_price
-
- # Method 4: Absolute fallback - return a small positive value to avoid 0
- logger.error(f"โ Could not estimate entry price for {symbol}, using fallback value of $1.00")
- return 1.0
-
- except Exception as e:
- logger.error(f"โ Error estimating entry price for {symbol}: {e}")
- return 1.0 # Safe fallback
|