#!/usr/bin/env python3
"""
Info Commands - Handles information-related Telegram commands.
"""
import logging
from datetime import datetime, timezone, timedelta
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__)
def _normalize_token_case(token: str) -> str:
"""
Normalize token case: if any characters are already uppercase, keep as-is.
Otherwise, convert to uppercase. This handles mixed-case tokens like kPEPE, kBONK.
"""
# Check if any character is already uppercase
if any(c.isupper() for c in token):
return token # Keep original case for mixed-case tokens
else:
return token.upper() # Convert to uppercase for all-lowercase input
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 = "๐ฐ Account Balance\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"๐ต USDC:\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 += "๐ Other Assets:\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"๐ต {asset}:\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"๐ผ Portfolio Summary:\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"๐ Performance:\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"๐ Position Auto-Synced\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"๐ Auto-synced {len(synced_positions)} orphaned position(s): {', '.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"๐ Open Positions\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} {token} ({direction}){type_indicator}\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"๐ผ Total Portfolio:\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"๐ค Legend: ๐ค 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 = "๐ Open Orders\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"๐ {symbol}\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"๐ผ Total Orders: {len(orders)}\n"
orders_text += f"๐ก Use /coo [token] to cancel orders"
else:
orders_text = "๐ Open Orders\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("๐ No trades found.", parse_mode='HTML')
return
message = "๐ Recent Trades (Last 20)\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} {side} {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("๐ No completed trade cycles found.", 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 = "๐ Trade Cycle Statistics\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"๐ Performance Summary:\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"๐ข Open Cycles ({len(open_cycles)}):\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"๐ Recent Completed ({len(completed_recent)}):\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 (using open positions)."""
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 open positions from unified trades table (current active trades)
open_positions = stats.get_open_positions()
if not open_positions:
await context.bot.send_message(
chat_id=chat_id,
text="๐ Active Positions\n\n๐ญ No active positions found.\n\n๐ก Use /long or /short to open positions.",
parse_mode='HTML'
)
return
message_text = "๐ Active Positions\n\n"
# Show each position
for position in open_positions:
symbol = position['symbol']
token = symbol.split('/')[0] if '/' in symbol else symbol
position_side = position['position_side'] # 'long' or 'short'
entry_price = position['entry_price']
current_amount = position['current_position_size']
trade_type = position.get('trade_type', 'manual')
# Position emoji and formatting
if position_side == 'long':
pos_emoji = "๐ข"
direction = "LONG"
else: # Short position
pos_emoji = "๐ด"
direction = "SHORT"
# Trade type indicator
type_indicator = ""
if trade_type == 'external':
type_indicator = " ๐" # External/synced position
elif trade_type == 'bot':
type_indicator = " ๐ค" # Bot-created position
message_text += f"{pos_emoji} {token} ({direction}){type_indicator}\n"
message_text += f" ๐ Size: {abs(current_amount):.6f} {token}\n"
message_text += f" ๐ฐ Entry: ${entry_price:.4f}\n"
# Show stop loss if linked
if position.get('stop_loss_price'):
sl_price = position['stop_loss_price']
sl_status = "Pending" if not position.get('stop_loss_order_id') else "Active"
message_text += f" ๐ Stop Loss: ${sl_price:.4f} ({sl_status})\n"
# Show take profit if linked
if position.get('take_profit_price'):
tp_price = position['take_profit_price']
tp_status = "Pending" if not position.get('take_profit_order_id') else "Active"
message_text += f" ๐ฏ Take Profit: ${tp_price:.4f} ({tp_status})\n"
message_text += f" ๐ Lifecycle ID: {position['trade_lifecycle_id'][:8]}\n\n"
# Add summary
total_positions = len(open_positions)
bot_positions = len([p for p in open_positions if p.get('trade_type') == 'bot'])
external_positions = len([p for p in open_positions if p.get('trade_type') == 'external'])
message_text += f"๐ Summary:\n"
message_text += f" Total: {total_positions} | "
message_text += f"Bot: {bot_positions} | "
message_text += f"External: {external_positions}\n\n"
message_text += f"๐ค Legend: ๐ค Bot-created โข ๐ External/synced\n"
message_text += f"๐ก Use /positions for detailed P&L information"
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 positions command: {str(e)}"
await context.bot.send_message(chat_id=chat_id, text=error_message)
logger.error(f"Error in active positions 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 = _normalize_token_case(context.args[0])
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"""
๐ {token} Market Data
๐ฐ Price Information:
๐ต Current: {current_price_str}
๐ข Bid: {bid_price_str}
๐ด Ask: {ask_price_str}
๐ Spread: {spread_str}
๐ 24h Statistics:
{trend_emoji} Change: {change_24h_str} ({change_percent:+.2f}%)
๐ High: {high_24h_str}
๐ป Low: {low_24h_str}
๐ Volume: {volume_24h:,.2f} {token}
โฐ Last Updated: {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 = _normalize_token_case(context.args[0])
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"""
๐ต {token} Price
๐ฐ {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 = _normalize_token_case(context.args[0])
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=
"๐ Token Performance\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 = "๐ Token Performance Ranking\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} {token}\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"๐ผ Portfolio Summary:\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"๐ก Usage: /performance BTC
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"๐ {token} Performance\n\n"
f"๐ญ No trading history found for {token}.\n\n"
f"๐ก Start trading {token} with:\n"
f"โข /long {token} 100
\n"
f"โข /short {token} 100
\n\n"
f"๐ Use /performance
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"๐ {token} Performance\n\n"
f"{token_stats['message']}\n\n"
f"๐ Current Activity:\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 /performance
to see all token rankings.",
parse_mode='HTML'
)
return
# Detailed stats display
pnl_emoji = "๐ข" if token_stats['total_pnl'] >= 0 else "๐ด"
performance_text = f"""
๐ {token} Detailed Performance
๐ฐ P&L Summary:
โข {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}
๐ Trading Activity:
โข Total Trades: {token_stats['total_trades']}
โข Completed: {token_stats['completed_trades']}
โข Buy Orders: {token_stats['buy_trades']}
โข Sell Orders: {token_stats['sell_trades']}
๐ Performance Metrics:
โข Win Rate: {token_stats['win_rate']:.1f}%
โข Profit Factor: {token_stats['profit_factor']:.2f}
โข Wins: {token_stats['total_wins']} | Losses: {token_stats['total_losses']}
๐ก Best/Worst:
โข 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๐ Recent Trades:\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 /performance
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=
"๐
Daily Performance\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 = "๐
Daily Performance (Last 10 Days)\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"๐ {day_stats['date_formatted']}\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"๐ {day_stats['date_formatted']}\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"๐ Period Summary:\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=
"๐ Weekly Performance\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 = "๐ Weekly Performance (Last 10 Weeks)\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"๐ {week_stats['week_formatted']}\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"๐ {week_stats['week_formatted']}\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"๐ผ 10-Week Summary:\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"๐ผ 10-Week Summary:\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=
"๐ Monthly Performance\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 = "๐ Monthly Performance (Last 10 Months)\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"๐
{month_stats['month_formatted']}\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"๐
{month_stats['month_formatted']}\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"๐ผ 10-Month Summary:\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"๐ผ 10-Month Summary:\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=
"๐ Risk Analysis\n\n"
"๐ญ Insufficient Data\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"
"๐ก To enable risk analysis:\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"""
๐ Risk Analysis & Advanced Metrics
๐ฏ Risk-Adjusted Performance:
โข Sharpe Ratio: {risk_metrics['sharpe_ratio']:.3f}
โข Sortino Ratio: {risk_metrics['sortino_ratio']:.3f}
โข Annual Volatility: {risk_metrics['volatility']:.2f}%
๐ Drawdown Analysis:
โข Maximum Drawdown: {risk_metrics['max_drawdown']:.2f}%
โข Value at Risk (95%): {risk_metrics['var_95']:.2f}%
๐ฐ Portfolio Context:
โข 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']}
๐ Risk Interpretation:
"""
# Add interpretive guidance
sharpe = risk_metrics['sharpe_ratio']
if sharpe > 2.0:
risk_text += "โข ๐ข Excellent risk-adjusted returns (Sharpe > 2.0)\n"
elif sharpe > 1.0:
risk_text += "โข ๐ก Good risk-adjusted returns (Sharpe > 1.0)\n"
elif sharpe > 0.5:
risk_text += "โข ๐ Moderate risk-adjusted returns (Sharpe > 0.5)\n"
elif sharpe > 0:
risk_text += "โข ๐ด Poor risk-adjusted returns (Sharpe > 0)\n"
else:
risk_text += "โข โซ Negative risk-adjusted returns (Sharpe < 0)\n"
max_dd = risk_metrics['max_drawdown']
if max_dd < 5:
risk_text += "โข ๐ข Low maximum drawdown (< 5%)\n"
elif max_dd < 15:
risk_text += "โข ๐ก Moderate maximum drawdown (< 15%)\n"
elif max_dd < 30:
risk_text += "โข ๐ High maximum drawdown (< 30%)\n"
else:
risk_text += "โข ๐ด Very High maximum drawdown (> 30%)\n"
volatility = risk_metrics['volatility']
if volatility < 10:
risk_text += "โข ๐ข Low portfolio volatility (< 10%)\n"
elif volatility < 25:
risk_text += "โข ๐ก Moderate portfolio volatility (< 25%)\n"
elif volatility < 50:
risk_text += "โข ๐ High portfolio volatility (< 50%)\n"
else:
risk_text += "โข ๐ด Very High portfolio volatility (> 50%)\n"
risk_text += f"""
๐ก Risk Definitions:
โข Sharpe Ratio: Risk-adjusted return (excess return / volatility)
โข Sortino Ratio: Return / downside volatility (focuses on bad volatility)
โข Max Drawdown: Largest peak-to-trough decline
โข VaR 95%: Maximum expected loss 95% of the time
โข Volatility: Annualized standard deviation of returns
๐ Data Based On:
โข 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=
"๐ฐ Balance Adjustments\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"""
๐ฐ Balance Adjustments History
๐ Summary:
โข 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']}
๐
Recent Adjustments:
"""
# 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"""
๐ก How it Works:
โข 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
โฐ Last Check: {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 = """
๐ฑ Quick Commands
Tap any button below for instant access to bot functions:
๐ก Pro Tip: 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