12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978 |
- #!/usr/bin/env python3
- """
- Telegram Bot for Hyperliquid Trading
- This module provides a Telegram interface for manual Hyperliquid trading
- with comprehensive statistics tracking and phone-friendly controls.
- """
- import logging
- import asyncio
- import re
- from datetime import datetime, timedelta
- from typing import Optional, Dict, Any
- from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
- from telegram.ext import Application, CommandHandler, CallbackQueryHandler, ContextTypes, MessageHandler, filters
- from hyperliquid_client import HyperliquidClient
- from trading_stats import TradingStats
- from config import Config
- from alarm_manager import AlarmManager
- from logging_config import setup_logging, cleanup_logs, format_log_stats
- # Set up logging using the new configuration system
- logger = setup_logging().getChild(__name__)
- class TelegramTradingBot:
- """Telegram bot for manual trading with comprehensive statistics."""
-
- def __init__(self):
- """Initialize the Telegram trading bot."""
- self.client = HyperliquidClient(use_testnet=Config.HYPERLIQUID_TESTNET)
- self.stats = TradingStats()
- self.alarm_manager = AlarmManager()
- self.authorized_chat_id = Config.TELEGRAM_CHAT_ID
- self.application = None
-
- # Order monitoring
- self.monitoring_active = False
- self.last_known_orders = set() # Track order IDs we've seen
- self.last_known_positions = {} # Track position sizes for P&L calculation
-
- # External trade monitoring
- self.last_processed_trade_time = None # Track last processed external trade
-
- # Initialize stats with current balance
- self._initialize_stats()
-
- def _initialize_stats(self):
- """Initialize stats with current balance."""
- try:
- balance = self.client.get_balance()
- if balance and balance.get('total'):
- # Get USDC balance as the main balance
- usdc_balance = float(balance['total'].get('USDC', 0))
- self.stats.set_initial_balance(usdc_balance)
- except Exception as e:
- logger.error(f"Could not initialize stats: {e}")
-
- def is_authorized(self, chat_id: str) -> bool:
- """Check if the chat ID is authorized to use the bot."""
- return str(chat_id) == str(self.authorized_chat_id)
-
- async def send_message(self, text: str, parse_mode: str = 'HTML') -> None:
- """Send a message to the authorized chat."""
- if self.application and self.authorized_chat_id:
- try:
- await self.application.bot.send_message(
- chat_id=self.authorized_chat_id,
- text=text,
- parse_mode=parse_mode
- )
- except Exception as e:
- logger.error(f"Failed to send message: {e}")
-
- async def start_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /start command."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("โ Unauthorized access.")
- return
-
- welcome_text = """
- ๐ค <b>Hyperliquid Manual Trading Bot</b>
- Welcome to your personal trading assistant! Control your Hyperliquid account directly from your phone.
- <b>๐ฑ Quick Actions:</b>
- Tap the buttons below for instant access to key functions.
- <b>๐ผ Account Commands:</b>
- /balance - Account balance
- /positions - Open positions
- /orders - Open orders
- /stats - Trading statistics
- <b>๐ Market Commands:</b>
- /market - Market data (default token)
- /market SOL - Market data for SOL
- /price - Current price (default token)
- /price BTC - Price for BTC
- <b>๐ Perps Trading:</b>
- โข /long BTC 100 - Long BTC with $100 USDC (Market Order)
- โข /long BTC 100 45000 - Long BTC with $100 USDC at $45,000 (Limit Order)
- โข /short ETH 50 - Short ETH with $50 USDC (Market Order)
- โข /short ETH 50 3500 - Short ETH with $50 USDC at $3,500 (Limit Order)
- โข /exit BTC - Close BTC position with Market Order
- <b>๐ก๏ธ Risk Management:</b>
- โข /sl BTC 44000 - Set stop loss for BTC at $44,000
- โข /tp BTC 50000 - Set take profit for BTC at $50,000
- <b>๐จ Automatic Stop Loss:</b>
- โข Enabled: {risk_enabled}
- โข Stop Loss: {stop_loss}% (automatic execution)
- โข Monitoring: Every {heartbeat} seconds
- <b>๐ Order Management:</b>
- โข /orders - Show all open orders
- โข /orders BTC - Show open orders for BTC only
- โข /coo BTC - Cancel all open orders for BTC
- <b>๐ Statistics & Analytics:</b>
- โข /stats - Complete trading statistics
- โข /performance - Token performance ranking & detailed stats
- โข /daily - Daily performance (last 10 days)
- โข /weekly - Weekly performance (last 10 weeks)
- โข /monthly - Monthly performance (last 10 months)
- โข /risk - Sharpe ratio, drawdown, VaR
- โข /trades - Recent trade history
- <b>๐ Price Alerts:</b>
- โข /alarm - List all alarms
- โข /alarm BTC 50000 - Set alarm for BTC at $50,000
- โข /alarm BTC - Show BTC alarms only
- โข /alarm 3 - Remove alarm ID 3
- <b>๐ Automatic Monitoring:</b>
- โข Real-time order fill alerts
- โข Position opened/closed notifications
- โข P&L calculations on trade closure
- โข Price alarm triggers
- โข External trade detection & sync
- โข Auto stats synchronization
- โข {heartbeat}-second monitoring interval
- <b>๐ Universal Trade Tracking:</b>
- โข Bot trades: Full logging & notifications
- โข Platform trades: Auto-detected & synced
- โข Mobile app trades: Monitored & recorded
- โข API trades: Tracked & included in stats
- Type /help for detailed command information.
- <b>๐ Order Monitoring:</b>
- โข /monitoring - View monitoring status
- โข /logs - View log file statistics and cleanup
- <b>โ๏ธ Configuration:</b>
- โข Symbol: {symbol}
- โข Default Token: {symbol}
- โข Network: {network}
- <b>๐ก๏ธ Safety Features:</b>
- โข All trades logged automatically
- โข Comprehensive performance tracking
- โข Real-time balance monitoring
- โข Risk metrics calculation
- <b>๐ฑ Mobile Optimized:</b>
- โข Quick action buttons
- โข Instant notifications
- โข Clean, readable layout
- โข One-tap commands
- For support, contact your bot administrator.
- """.format(
- symbol=Config.DEFAULT_TRADING_TOKEN,
- network="Testnet" if Config.HYPERLIQUID_TESTNET else "Mainnet",
- risk_enabled=Config.RISK_MANAGEMENT_ENABLED,
- stop_loss=Config.STOP_LOSS_PERCENTAGE,
- heartbeat=Config.BOT_HEARTBEAT_SECONDS
- )
-
- keyboard = [
- [
- InlineKeyboardButton("๐ฐ Balance", callback_data="balance"),
- InlineKeyboardButton("๐ Stats", callback_data="stats")
- ],
- [
- InlineKeyboardButton("๐ Positions", callback_data="positions"),
- InlineKeyboardButton("๐ Orders", callback_data="orders")
- ],
- [
- InlineKeyboardButton("๐ต Price", callback_data="price"),
- InlineKeyboardButton("๐ Market", callback_data="market")
- ],
- [
- InlineKeyboardButton("๐ Recent Trades", callback_data="trades"),
- InlineKeyboardButton("โ๏ธ Help", callback_data="help")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await update.message.reply_text(welcome_text, parse_mode='HTML', reply_markup=reply_markup)
-
- async def help_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /help command."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("โ Unauthorized access.")
- return
-
- help_text = """
- ๐ง <b>Hyperliquid Trading Bot - Complete Guide</b>
- <b>๐ผ Account Management:</b>
- โข /balance - Show account balance
- โข /positions - Show open positions
- โข /orders - Show open orders
- <b>๐ Market Data:</b>
- โข /market - Detailed market data (default token)
- โข /market BTC - Market data for specific token
- โข /price - Quick price check (default token)
- โข /price SOL - Price for specific token
- <b>๐ Perps Trading:</b>
- โข /long BTC 100 - Long BTC with $100 USDC (Market Order)
- โข /long BTC 100 45000 - Long BTC with $100 USDC at $45,000 (Limit Order)
- โข /short ETH 50 - Short ETH with $50 USDC (Market Order)
- โข /short ETH 50 3500 - Short ETH with $50 USDC at $3,500 (Limit Order)
- โข /exit BTC - Close BTC position with Market Order
- <b>๐ก๏ธ Risk Management:</b>
- โข /sl BTC 44000 - Set stop loss for BTC at $44,000
- โข /tp BTC 50000 - Set take profit for BTC at $50,000
- <b>๐จ Automatic Stop Loss:</b>
- โข Enabled: {risk_enabled}
- โข Stop Loss: {stop_loss}% (automatic execution)
- โข Monitoring: Every {heartbeat} seconds
- <b>๐ Order Management:</b>
- โข /orders - Show all open orders
- โข /orders BTC - Show open orders for BTC only
- โข /coo BTC - Cancel all open orders for BTC
- <b>๐ Statistics & Analytics:</b>
- โข /stats - Complete trading statistics
- โข /performance - Win rate, profit factor, etc.
- โข /risk - Sharpe ratio, drawdown, VaR
- โข /trades - Recent trade history
- <b>๐ Price Alerts:</b>
- โข /alarm - List all active alarms
- โข /alarm BTC 50000 - Set alarm for BTC at $50,000
- โข /alarm BTC - Show all BTC alarms
- โข /alarm 3 - Remove alarm ID 3
- <b>๐ Order Monitoring:</b>
- โข /monitoring - View monitoring status
- โข /logs - View log file statistics and cleanup
- <b>โ๏ธ Configuration:</b>
- โข Symbol: {symbol}
- โข Default Token: {symbol}
- โข Network: {network}
- <b>๐ก๏ธ Safety Features:</b>
- โข All trades logged automatically
- โข Comprehensive performance tracking
- โข Real-time balance monitoring
- โข Risk metrics calculation
- <b>๐ฑ Mobile Optimized:</b>
- โข Quick action buttons
- โข Instant notifications
- โข Clean, readable layout
- โข One-tap commands
- For support, contact your bot administrator.
- """.format(
- symbol=Config.DEFAULT_TRADING_TOKEN,
- network="Testnet" if Config.HYPERLIQUID_TESTNET else "Mainnet",
- risk_enabled=Config.RISK_MANAGEMENT_ENABLED,
- stop_loss=Config.STOP_LOSS_PERCENTAGE,
- heartbeat=Config.BOT_HEARTBEAT_SECONDS
- )
-
- await update.message.reply_text(help_text, parse_mode='HTML')
-
- async def stats_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /stats command."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("โ Unauthorized access.")
- return
-
- # Get current balance for stats
- balance = self.client.get_balance()
- current_balance = 0
- if balance and balance.get('total'):
- current_balance = float(balance['total'].get('USDC', 0))
-
- stats_message = self.stats.format_stats_message(current_balance)
- await update.message.reply_text(stats_message, parse_mode='HTML')
-
- async def trades_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /trades command."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("โ Unauthorized access.")
- return
-
- recent_trades = self.stats.get_recent_trades(10)
-
- if not recent_trades:
- await update.message.reply_text("๐ No trades recorded yet.")
- return
-
- trades_text = "๐ <b>Recent Trades</b>\n\n"
-
- for trade in reversed(recent_trades[-5:]): # Show last 5 trades
- timestamp = datetime.fromisoformat(trade['timestamp']).strftime('%m/%d %H:%M')
- side_emoji = "๐ข" if trade['side'] == 'buy' else "๐ด"
-
- trades_text += f"{side_emoji} <b>{trade['side'].upper()}</b> {trade['amount']} {trade['symbol']}\n"
- trades_text += f" ๐ฐ ${trade['price']:,.2f} | ๐ต ${trade['value']:,.2f}\n"
- trades_text += f" ๐
{timestamp}\n\n"
-
- await update.message.reply_text(trades_text, parse_mode='HTML')
-
- async def balance_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /balance command."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("โ Unauthorized access.")
- return
-
- balance = self.client.get_balance()
- if balance:
- balance_text = "๐ฐ <b>Account Balance</b>\n\n"
-
- # CCXT balance structure includes 'free', 'used', and 'total'
- total_balance = balance.get('total', {})
- free_balance = balance.get('free', {})
- used_balance = balance.get('used', {})
-
- if total_balance:
- total_value = 0
- available_value = 0
-
- # Display individual assets
- for asset, amount in total_balance.items():
- if float(amount) > 0:
- 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}\n"
- balance_text += f" โ
Available: {free_amount}\n"
-
- if used_amount > 0:
- balance_text += f" ๐ In Use: {used_amount}\n"
-
- balance_text += "\n"
-
- # Calculate totals for USDC (main trading currency)
- if asset == 'USDC':
- total_value += float(amount)
- available_value += free_amount
-
- # Summary section
- balance_text += f"๐ผ <b>Portfolio Summary:</b>\n"
- balance_text += f" ๐ฐ Total Value: ${total_value:,.2f}\n"
- balance_text += f" ๐ Available for Trading: ${available_value:,.2f}\n"
-
- if total_value - available_value > 0:
- balance_text += f" ๐ In Active Use: ${total_value - available_value:,.2f}\n"
-
- # Add P&L summary
- basic_stats = self.stats.get_basic_stats()
- if basic_stats['initial_balance'] > 0:
- pnl = total_value - basic_stats['initial_balance']
- pnl_percent = (pnl / basic_stats['initial_balance']) * 100
-
- balance_text += f"\n๐ <b>Performance:</b>\n"
- balance_text += f" ๐ต P&L: ${pnl:,.2f} ({pnl_percent:+.2f}%)\n"
- balance_text += f" ๐ Initial: ${basic_stats['initial_balance']:,.2f}"
- else:
- balance_text += "๐ญ No balance data available"
- else:
- balance_text = "โ Could not fetch balance data"
-
- await update.message.reply_text(balance_text, parse_mode='HTML')
-
- async def positions_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /positions command."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("โ Unauthorized access.")
- return
-
- positions = self.client.get_positions()
-
- if positions is not None: # Successfully fetched (could be empty list)
- positions_text = "๐ <b>Open Positions</b>\n\n"
-
- # Filter for actual open positions
- open_positions = [p for p in positions if float(p.get('contracts', 0)) != 0]
-
- if open_positions:
- total_unrealized = 0
- total_position_value = 0
-
- for position in open_positions:
- symbol = position.get('symbol', 'Unknown')
- contracts = float(position.get('contracts', 0))
- unrealized_pnl = float(position.get('unrealizedPnl', 0))
- entry_price = float(position.get('entryPx', 0))
-
- # Calculate position value and P&L percentage
- position_value = abs(contracts) * entry_price
- pnl_percentage = (unrealized_pnl / position_value * 100) if position_value > 0 else 0
-
- pnl_emoji = "๐ข" if unrealized_pnl >= 0 else "๐ด"
-
- # Extract token name for cleaner display
- token = symbol.split('/')[0] if '/' in symbol else symbol
- position_type = "LONG" if contracts > 0 else "SHORT"
-
- positions_text += f"๐ <b>{token}</b> ({position_type})\n"
- positions_text += f" ๐ Size: {abs(contracts):.6f} {token}\n"
- positions_text += f" ๐ฐ Entry: ${entry_price:,.2f}\n"
- positions_text += f" ๐ต Value: ${position_value:,.2f}\n"
- positions_text += f" {pnl_emoji} P&L: ${unrealized_pnl:,.2f} ({pnl_percentage:+.2f}%)\n\n"
-
- total_unrealized += unrealized_pnl
- total_position_value += position_value
-
- # Calculate overall P&L percentage
- total_pnl_percentage = (total_unrealized / total_position_value * 100) if total_position_value > 0 else 0
- total_pnl_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" {total_pnl_emoji} Total P&L: ${total_unrealized:,.2f} ({total_pnl_percentage:+.2f}%)"
- else:
- positions_text += "๐ญ <b>No open positions currently</b>\n\n"
- positions_text += "๐ Ready to start trading!\n"
- positions_text += "Use /buy or /sell commands to open positions."
- else:
- # Actual API error
- positions_text = "โ <b>Could not fetch positions data</b>\n\n"
- positions_text += "๐ Please try again in a moment.\n"
- positions_text += "If the issue persists, check your connection."
-
- await update.message.reply_text(positions_text, parse_mode='HTML')
-
- async def orders_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /orders command with optional token filter."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("โ Unauthorized access.")
- return
-
- # Check if token filter is provided
- token_filter = None
- if context.args and len(context.args) >= 1:
- token_filter = context.args[0].upper()
-
- orders = self.client.get_open_orders()
-
- if orders is not None: # Successfully fetched (could be empty list)
- if token_filter:
- orders_text = f"๐ <b>Open Orders - {token_filter}</b>\n\n"
- # Filter orders for specific token
- target_symbol = f"{token_filter}/USDC:USDC"
- filtered_orders = [order for order in orders if order.get('symbol') == target_symbol]
- else:
- orders_text = "๐ <b>All Open Orders</b>\n\n"
- filtered_orders = orders
-
- if filtered_orders and len(filtered_orders) > 0:
- for order in filtered_orders:
- symbol = order.get('symbol', 'Unknown')
- side = order.get('side', 'Unknown')
- amount = order.get('amount', 0)
- price = order.get('price', 0)
- order_id = order.get('id', 'Unknown')
-
- # Extract token from symbol for display
- token = symbol.split('/')[0] if '/' in symbol else symbol
-
- side_emoji = "๐ข" if side.lower() == 'buy' else "๐ด"
-
- orders_text += f"{side_emoji} <b>{token}</b>\n"
- orders_text += f" ๐ {side.upper()} {amount} @ ${price:,.2f}\n"
- orders_text += f" ๐ต Value: ${float(amount) * float(price):,.2f}\n"
- orders_text += f" ๐ ID: <code>{order_id}</code>\n\n"
-
- # Add helpful commands
- if token_filter:
- orders_text += f"๐ก <b>Quick Actions:</b>\n"
- orders_text += f"โข <code>/coo {token_filter}</code> - Cancel all {token_filter} orders\n"
- orders_text += f"โข <code>/orders</code> - View all orders"
- else:
- orders_text += f"๐ก <b>Filter by token:</b> <code>/orders BTC</code>, <code>/orders ETH</code>"
- else:
- if token_filter:
- orders_text += f"๐ญ <b>No open orders for {token_filter}</b>\n\n"
- orders_text += f"๐ก No pending {token_filter} orders found.\n"
- orders_text += f"Use <code>/long {token_filter} 100</code> or <code>/short {token_filter} 100</code> to create new orders."
- else:
- orders_text += "๐ญ <b>No open orders currently</b>\n\n"
- orders_text += "๐ก All clear! No pending orders.\n"
- orders_text += "Use /long or /short commands to place new orders."
- else:
- # Actual API error
- orders_text = "โ <b>Could not fetch orders data</b>\n\n"
- orders_text += "๐ Please try again in a moment.\n"
- orders_text += "If the issue persists, check your connection."
-
- await update.message.reply_text(orders_text, parse_mode='HTML')
-
- async def market_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /market command."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("โ Unauthorized access.")
- return
-
- # Check if token is provided as argument
- if context.args and len(context.args) >= 1:
- token = context.args[0].upper()
- else:
- token = Config.DEFAULT_TRADING_TOKEN
-
- # Convert token to full symbol format for API
- symbol = f"{token}/USDC:USDC"
- market_data = self.client.get_market_data(symbol)
-
- if market_data and market_data.get('ticker'):
- try:
- ticker = market_data['ticker']
- orderbook = market_data.get('orderbook', {})
-
- # Safely extract ticker data with fallbacks
- current_price = float(ticker.get('last') or 0)
- high_24h = float(ticker.get('high') or 0)
- low_24h = float(ticker.get('low') or 0)
- volume_24h = ticker.get('baseVolume') or ticker.get('volume') or 'N/A'
-
- market_text = f"๐ <b>Market Data - {token}</b>\n\n"
-
- if current_price > 0:
- market_text += f"๐ต <b>Current Price:</b> ${current_price:,.2f}\n"
- else:
- market_text += f"๐ต <b>Current Price:</b> N/A\n"
-
- if high_24h > 0:
- market_text += f"๐ <b>24h High:</b> ${high_24h:,.2f}\n"
- else:
- market_text += f"๐ <b>24h High:</b> N/A\n"
-
- if low_24h > 0:
- market_text += f"๐ <b>24h Low:</b> ${low_24h:,.2f}\n"
- else:
- market_text += f"๐ <b>24h Low:</b> N/A\n"
-
- market_text += f"๐ <b>24h Volume:</b> {volume_24h}\n\n"
-
- # Handle orderbook data safely
- if orderbook and orderbook.get('bids') and orderbook.get('asks'):
- try:
- bids = orderbook.get('bids', [])
- asks = orderbook.get('asks', [])
-
- if bids and asks and len(bids) > 0 and len(asks) > 0:
- best_bid = float(bids[0][0]) if bids[0][0] else 0
- best_ask = float(asks[0][0]) if asks[0][0] else 0
-
- if best_bid > 0 and best_ask > 0:
- spread = best_ask - best_bid
- spread_percent = (spread / best_ask * 100) if best_ask > 0 else 0
-
- market_text += f"๐ข <b>Best Bid:</b> ${best_bid:,.2f}\n"
- market_text += f"๐ด <b>Best Ask:</b> ${best_ask:,.2f}\n"
- market_text += f"๐ <b>Spread:</b> ${spread:.2f} ({spread_percent:.3f}%)\n"
- else:
- market_text += f"๐ <b>Orderbook:</b> Data unavailable\n"
- else:
- market_text += f"๐ <b>Orderbook:</b> No orders available\n"
- except (IndexError, ValueError, TypeError) as e:
- market_text += f"๐ <b>Orderbook:</b> Error parsing data\n"
- else:
- market_text += f"๐ <b>Orderbook:</b> Not available\n"
-
- # Add usage hint
- market_text += f"\n๐ก <b>Usage:</b> <code>/market {token}</code> or <code>/market</code> for default"
-
- except (ValueError, TypeError) as e:
- market_text = f"โ <b>Error parsing market data</b>\n\n"
- market_text += f"๐ง Raw data received but couldn't parse values.\n"
- market_text += f"๐ Please try again or contact support if this persists."
- else:
- market_text = f"โ <b>Could not fetch market data for {token}</b>\n\n"
- market_text += f"๐ Please try again in a moment.\n"
- market_text += f"๐ Check your network connection.\n"
- market_text += f"๐ก API may be temporarily unavailable.\n\n"
- market_text += f"๐ก <b>Usage:</b> <code>/market BTC</code>, <code>/market ETH</code>, <code>/market SOL</code>, etc."
-
- await update.message.reply_text(market_text, parse_mode='HTML')
-
- async def price_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /price command."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("โ Unauthorized access.")
- return
-
- # Check if token is provided as argument
- if context.args and len(context.args) >= 1:
- token = context.args[0].upper()
- else:
- token = Config.DEFAULT_TRADING_TOKEN
-
- # Convert token to full symbol format for API
- symbol = f"{token}/USDC:USDC"
- market_data = self.client.get_market_data(symbol)
-
- if market_data and market_data.get('ticker'):
- try:
- ticker = market_data['ticker']
- price_value = ticker.get('last')
-
- if price_value is not None:
- price = float(price_value)
- price_text = f"๐ต <b>{token}</b>: ${price:,.2f}"
-
- # Add timestamp
- timestamp = datetime.now().strftime('%H:%M:%S')
- price_text += f"\nโฐ <i>Updated: {timestamp}</i>"
-
- # Add usage hint
- price_text += f"\n๐ก <i>Usage: </i><code>/price {symbol}</code><i> or </i><code>/price</code><i> for default</i>"
- else:
- price_text = f"๐ต <b>{symbol}</b>: Price not available\nโ ๏ธ <i>Data temporarily unavailable</i>"
-
- except (ValueError, TypeError) as e:
- price_text = f"โ <b>Error parsing price for {symbol}</b>\n๐ง <i>Please try again</i>"
- else:
- price_text = f"โ <b>Could not fetch price for {symbol}</b>\n๐ <i>Please try again in a moment</i>\n\n"
- price_text += f"๐ก <b>Usage:</b> <code>/price BTC</code>, <code>/price ETH</code>, <code>/price SOL</code>, etc."
-
- await update.message.reply_text(price_text, parse_mode='HTML')
-
- async def button_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle inline keyboard button presses."""
- query = update.callback_query
- await query.answer()
-
- if not self.is_authorized(query.message.chat_id):
- await query.edit_message_text("โ Unauthorized access.")
- return
-
- callback_data = query.data
-
- # Handle trading confirmations
- if callback_data.startswith('confirm_long_'):
- parts = callback_data.split('_')
- token = parts[2]
- usdc_amount = float(parts[3])
- price = float(parts[4])
- is_limit = len(parts) > 5 and parts[5] == 'limit'
- await self._execute_long_order(query, token, usdc_amount, price, is_limit)
- return
-
- elif callback_data.startswith('confirm_short_'):
- parts = callback_data.split('_')
- token = parts[2]
- usdc_amount = float(parts[3])
- price = float(parts[4])
- is_limit = len(parts) > 5 and parts[5] == 'limit'
- await self._execute_short_order(query, token, usdc_amount, price, is_limit)
- return
-
- elif callback_data.startswith('confirm_exit_'):
- parts = callback_data.split('_')
- token = parts[2]
- exit_side = parts[3]
- contracts = float(parts[4])
- price = float(parts[5])
- await self._execute_exit_order(query, token, exit_side, contracts, price)
- return
-
- elif callback_data.startswith('confirm_coo_'):
- parts = callback_data.split('_')
- token = parts[2]
- await self._execute_coo(query, token)
- return
-
- elif callback_data.startswith('confirm_sl_'):
- parts = callback_data.split('_')
- token = parts[2]
- exit_side = parts[3]
- contracts = float(parts[4])
- price = float(parts[5])
- await self._execute_sl_order(query, token, exit_side, contracts, price)
- return
-
- elif callback_data.startswith('confirm_tp_'):
- parts = callback_data.split('_')
- token = parts[2]
- exit_side = parts[3]
- contracts = float(parts[4])
- price = float(parts[5])
- await self._execute_tp_order(query, token, exit_side, contracts, price)
- return
-
- elif callback_data == 'cancel_order':
- await query.edit_message_text("โ Order cancelled.")
- return
-
- # Create a fake update object for reusing command handlers
- fake_update = Update(
- update_id=update.update_id,
- message=query.message,
- callback_query=query
- )
-
- # Handle regular button callbacks
- if callback_data == "balance":
- await self.balance_command(fake_update, context)
- elif callback_data == "stats":
- await self.stats_command(fake_update, context)
- elif callback_data == "positions":
- await self.positions_command(fake_update, context)
- elif callback_data == "orders":
- await self.orders_command(fake_update, context)
- elif callback_data == "market":
- await self.market_command(fake_update, context)
- elif callback_data == "price":
- await self.price_command(fake_update, context)
- elif callback_data == "trades":
- await self.trades_command(fake_update, context)
- elif callback_data == "help":
- await self.help_command(fake_update, context)
-
- async def _execute_long_order(self, query, token: str, usdc_amount: float, price: float, is_limit: bool):
- """Execute a long order."""
- symbol = f"{token}/USDC:USDC"
-
- try:
- await query.edit_message_text("โณ Opening long position...")
-
- # Calculate token amount based on USDC value and price
- token_amount = usdc_amount / price
-
- # Place order (limit or market)
- if is_limit:
- order = self.client.place_limit_order(symbol, 'buy', token_amount, price)
- else:
- order = self.client.place_market_order(symbol, 'buy', token_amount)
-
- if order:
- # Record the trade in stats
- order_id = order.get('id', 'N/A')
- actual_price = order.get('average', price) # Use actual fill price if available
- self.stats.record_trade(symbol, 'buy', token_amount, actual_price, order_id)
-
- success_message = f"""
- โ
<b>Long Position {'Placed' if is_limit else 'Opened'} Successfully!</b>
- ๐ <b>Order Details:</b>
- โข Token: {token}
- โข Direction: LONG (Buy)
- โข Amount: {token_amount:.6f} {token}
- โข Price: ${price:,.2f}
- โข USDC Value: ~${usdc_amount:,.2f}
- โข Order Type: {'Limit' if is_limit else 'Market'} Order
- โข Order ID: <code>{order_id}</code>
- ๐ Your {'limit order has been placed' if is_limit else 'long position is now active'}!
- """
-
- await query.edit_message_text(success_message, parse_mode='HTML')
- logger.info(f"Long {'limit order placed' if is_limit else 'position opened'}: {token_amount:.6f} {token} @ ${price} ({'Limit' if is_limit else 'Market'})")
- else:
- await query.edit_message_text(f"โ Failed to {'place limit order' if is_limit else 'open long position'}. Please try again.")
-
- except Exception as e:
- error_message = f"โ Error {'placing limit order' if is_limit else 'opening long position'}: {str(e)}"
- await query.edit_message_text(error_message)
- logger.error(f"Error in long order: {e}")
-
- async def _execute_short_order(self, query, token: str, usdc_amount: float, price: float, is_limit: bool):
- """Execute a short order."""
- symbol = f"{token}/USDC:USDC"
-
- try:
- await query.edit_message_text("โณ Opening short position...")
-
- # Calculate token amount based on USDC value and price
- token_amount = usdc_amount / price
-
- # Place order (limit or market)
- if is_limit:
- order = self.client.place_limit_order(symbol, 'sell', token_amount, price)
- else:
- order = self.client.place_market_order(symbol, 'sell', token_amount)
-
- if order:
- # Record the trade in stats
- order_id = order.get('id', 'N/A')
- actual_price = order.get('average', price) # Use actual fill price if available
- self.stats.record_trade(symbol, 'sell', token_amount, actual_price, order_id)
-
- success_message = f"""
- โ
<b>Short Position {'Placed' if is_limit else 'Opened'} Successfully!</b>
- ๐ <b>Order Details:</b>
- โข Token: {token}
- โข Direction: SHORT (Sell)
- โข Amount: {token_amount:.6f} {token}
- โข Price: ${price:,.2f}
- โข USDC Value: ~${usdc_amount:,.2f}
- โข Order Type: {'Limit' if is_limit else 'Market'} Order
- โข Order ID: <code>{order_id}</code>
- ๐ Your {'limit order has been placed' if is_limit else 'short position is now active'}!
- """
-
- await query.edit_message_text(success_message, parse_mode='HTML')
- logger.info(f"Short {'limit order placed' if is_limit else 'position opened'}: {token_amount:.6f} {token} @ ${price} ({'Limit' if is_limit else 'Market'})")
- else:
- await query.edit_message_text(f"โ Failed to {'place limit order' if is_limit else 'open short position'}. Please try again.")
-
- except Exception as e:
- error_message = f"โ Error {'placing limit order' if is_limit else 'opening short position'}: {str(e)}"
- await query.edit_message_text(error_message)
- logger.error(f"Error in short order: {e}")
-
- async def _execute_exit_order(self, query, token: str, exit_side: str, contracts: float, price: float):
- """Execute an exit order."""
- symbol = f"{token}/USDC:USDC"
-
- try:
- await query.edit_message_text("โณ Closing position...")
-
- # Place market order to close position
- order = self.client.place_market_order(symbol, exit_side, contracts)
-
- if order:
- # Record the trade in stats
- order_id = order.get('id', 'N/A')
- actual_price = order.get('average', price) # Use actual fill price if available
- self.stats.record_trade(symbol, exit_side, contracts, actual_price, order_id)
-
- position_type = "LONG" if exit_side == "sell" else "SHORT"
-
- success_message = f"""
- โ
<b>Position Closed Successfully!</b>
- ๐ <b>Exit Details:</b>
- โข Token: {token}
- โข Position Closed: {position_type}
- โข Exit Side: {exit_side.upper()}
- โข Amount: {contracts} {token}
- โข Est. Price: ~${price:,.2f}
- โข Order Type: Market Order
- โข Order ID: <code>{order_id}</code>
- ๐ฏ <b>Position Summary:</b>
- โข Status: CLOSED
- โข Exit Value: ~${contracts * price:,.2f}
- ๐ Use /stats to see updated performance metrics.
- """
-
- await query.edit_message_text(success_message, parse_mode='HTML')
- logger.info(f"Position closed: {exit_side} {contracts} {token} @ ~${price}")
- else:
- await query.edit_message_text("โ Failed to close position. Please try again.")
-
- except Exception as e:
- error_message = f"โ Error closing position: {str(e)}"
- await query.edit_message_text(error_message)
- logger.error(f"Error closing position: {e}")
-
- async def _execute_coo(self, query, token: str):
- """Execute cancel open orders for a specific token."""
- symbol = f"{token}/USDC:USDC"
-
- try:
- await query.edit_message_text("โณ Cancelling all orders...")
-
- # Get current orders for this token
- all_orders = self.client.get_open_orders()
- if all_orders is None:
- await query.edit_message_text(f"โ Could not fetch orders to cancel {token} orders")
- return
-
- # Filter orders for the specific token
- token_orders = [order for order in all_orders if order.get('symbol') == symbol]
-
- if not token_orders:
- await query.edit_message_text(f"๐ญ No open orders found for {token}")
- return
-
- # Cancel each order
- cancelled_orders = []
- failed_orders = []
-
- for order in token_orders:
- order_id = order.get('id')
- if order_id:
- try:
- success = self.client.cancel_order(order_id, symbol)
- if success:
- cancelled_orders.append(order)
- else:
- failed_orders.append(order)
- except Exception as e:
- logger.error(f"Failed to cancel order {order_id}: {e}")
- failed_orders.append(order)
-
- # Create result message
- result_message = f"""
- โ
<b>Cancel Orders Results</b>
- ๐ <b>Summary:</b>
- โข Token: {token}
- โข Cancelled: {len(cancelled_orders)} orders
- โข Failed: {len(failed_orders)} orders
- โข Total Attempted: {len(token_orders)} orders
- """
-
- if cancelled_orders:
- result_message += f"\n๐๏ธ <b>Successfully Cancelled:</b>\n"
- for order in cancelled_orders:
- side = order.get('side', 'Unknown')
- amount = order.get('amount', 0)
- price = order.get('price', 0)
- side_emoji = "๐ข" if side.lower() == 'buy' else "๐ด"
- result_message += f"{side_emoji} {side.upper()} {amount} @ ${price:,.2f}\n"
-
- if failed_orders:
- result_message += f"\nโ <b>Failed to Cancel:</b>\n"
- for order in failed_orders:
- side = order.get('side', 'Unknown')
- amount = order.get('amount', 0)
- price = order.get('price', 0)
- order_id = order.get('id', 'Unknown')
- side_emoji = "๐ข" if side.lower() == 'buy' else "๐ด"
- result_message += f"{side_emoji} {side.upper()} {amount} @ ${price:,.2f} (ID: {order_id})\n"
-
- if len(cancelled_orders) == len(token_orders):
- result_message += f"\n๐ All {token} orders successfully cancelled!"
- elif len(cancelled_orders) > 0:
- result_message += f"\nโ ๏ธ Some orders cancelled. Check failed orders above."
- else:
- result_message += f"\nโ Could not cancel any {token} orders."
-
- await query.edit_message_text(result_message, parse_mode='HTML')
- logger.info(f"COO executed for {token}: {len(cancelled_orders)}/{len(token_orders)} orders cancelled")
-
- except Exception as e:
- error_message = f"โ Error cancelling {token} orders: {str(e)}"
- await query.edit_message_text(error_message)
- logger.error(f"Error in COO execution: {e}")
-
- async def _execute_sl_order(self, query, token: str, exit_side: str, contracts: float, price: float):
- """Execute a stop loss order."""
- symbol = f"{token}/USDC:USDC"
-
- try:
- await query.edit_message_text("โณ Setting stop loss...")
-
- # Place stop loss order
- order = self.client.place_stop_loss_order(symbol, exit_side, contracts, price)
-
- if order:
- # Record the trade in stats
- order_id = order.get('id', 'N/A')
- actual_price = order.get('average', price) # Use actual fill price if available
- self.stats.record_trade(symbol, exit_side, contracts, actual_price, order_id)
-
- position_type = "LONG" if exit_side == "sell" else "SHORT"
-
- success_message = f"""
- โ
<b>Stop Loss Order Set Successfully!</b>
- ๐ <b>Stop Loss Details:</b>
- โข Token: {token}
- โข Position: {position_type}
- โข Size: {contracts} contracts
- โข Stop Price: ${price:,.2f}
- โข Action: {exit_side.upper()} (Close {position_type})
- โข Amount: {contracts} {token}
- โข Order Type: Limit Order
- โข Order ID: <code>{order_id}</code>
- ๐ฏ <b>Stop Loss Execution:</b>
- โข Status: SET
- โข Exit Value: ~${contracts * price:,.2f}
- ๐ Use /stats to see updated performance metrics.
- """
-
- await query.edit_message_text(success_message, parse_mode='HTML')
- logger.info(f"Stop loss set: {exit_side} {contracts} {token} @ ${price}")
- else:
- await query.edit_message_text("โ Failed to set stop loss. Please try again.")
-
- except Exception as e:
- error_message = f"โ Error setting stop loss: {str(e)}"
- await query.edit_message_text(error_message)
- logger.error(f"Error setting stop loss: {e}")
-
- async def _execute_tp_order(self, query, token: str, exit_side: str, contracts: float, price: float):
- """Execute a take profit order."""
- symbol = f"{token}/USDC:USDC"
-
- try:
- await query.edit_message_text("โณ Setting take profit...")
-
- # Place take profit order
- order = self.client.place_take_profit_order(symbol, exit_side, contracts, price)
-
- if order:
- # Record the trade in stats
- order_id = order.get('id', 'N/A')
- actual_price = order.get('average', price) # Use actual fill price if available
- self.stats.record_trade(symbol, exit_side, contracts, actual_price, order_id)
-
- position_type = "LONG" if exit_side == "sell" else "SHORT"
-
- success_message = f"""
- โ
<b>Take Profit Order Set Successfully!</b>
- ๐ <b>Take Profit Details:</b>
- โข Token: {token}
- โข Position: {position_type}
- โข Size: {contracts} contracts
- โข Target Price: ${price:,.2f}
- โข Action: {exit_side.upper()} (Close {position_type})
- โข Amount: {contracts} {token}
- โข Order Type: Limit Order
- โข Order ID: <code>{order_id}</code>
- ๐ฏ <b>Take Profit Execution:</b>
- โข Status: SET
- โข Exit Value: ~${contracts * price:,.2f}
- ๐ Use /stats to see updated performance metrics.
- """
-
- await query.edit_message_text(success_message, parse_mode='HTML')
- logger.info(f"Take profit set: {exit_side} {contracts} {token} @ ${price}")
- else:
- await query.edit_message_text("โ Failed to set take profit. Please try again.")
-
- except Exception as e:
- error_message = f"โ Error setting take profit: {str(e)}"
- await query.edit_message_text(error_message)
- logger.error(f"Error setting take profit: {e}")
-
- async def unknown_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle unknown commands."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("โ Unauthorized access.")
- return
-
- await update.message.reply_text(
- "โ Unknown command. Use /help to see available commands or tap the buttons in /start."
- )
-
- def setup_handlers(self):
- """Set up command handlers for the bot."""
- if not self.application:
- return
-
- # Command handlers
- self.application.add_handler(CommandHandler("start", self.start_command))
- self.application.add_handler(CommandHandler("help", self.help_command))
- self.application.add_handler(CommandHandler("balance", self.balance_command))
- self.application.add_handler(CommandHandler("positions", self.positions_command))
- self.application.add_handler(CommandHandler("orders", self.orders_command))
- self.application.add_handler(CommandHandler("market", self.market_command))
- self.application.add_handler(CommandHandler("price", self.price_command))
- self.application.add_handler(CommandHandler("stats", self.stats_command))
- self.application.add_handler(CommandHandler("trades", self.trades_command))
- self.application.add_handler(CommandHandler("long", self.long_command))
- self.application.add_handler(CommandHandler("short", self.short_command))
- self.application.add_handler(CommandHandler("exit", self.exit_command))
- self.application.add_handler(CommandHandler("coo", self.coo_command))
- self.application.add_handler(CommandHandler("sl", self.sl_command))
- self.application.add_handler(CommandHandler("tp", self.tp_command))
- self.application.add_handler(CommandHandler("monitoring", self.monitoring_command))
- self.application.add_handler(CommandHandler("alarm", self.alarm_command))
- self.application.add_handler(CommandHandler("logs", self.logs_command))
- self.application.add_handler(CommandHandler("performance", self.performance_command))
- self.application.add_handler(CommandHandler("daily", self.daily_command))
- self.application.add_handler(CommandHandler("weekly", self.weekly_command))
- self.application.add_handler(CommandHandler("monthly", self.monthly_command))
-
- # Callback query handler for inline keyboards
- self.application.add_handler(CallbackQueryHandler(self.button_callback))
-
- # Handle unknown commands
- self.application.add_handler(MessageHandler(filters.COMMAND, self.unknown_command))
-
- async def run(self):
- """Run the Telegram bot."""
- if not Config.TELEGRAM_BOT_TOKEN:
- logger.error("โ TELEGRAM_BOT_TOKEN not configured")
- return
-
- if not Config.TELEGRAM_CHAT_ID:
- logger.error("โ TELEGRAM_CHAT_ID not configured")
- return
-
- try:
- # Create application
- self.application = Application.builder().token(Config.TELEGRAM_BOT_TOKEN).build()
-
- # Set up handlers
- self.setup_handlers()
-
- logger.info("๐ Starting Telegram trading bot...")
-
- # Initialize the application
- await self.application.initialize()
-
- # Send startup notification
- await self.send_message(
- "๐ค <b>Manual Trading Bot Started</b>\n\n"
- f"โ
Connected to Hyperliquid {'Testnet' if Config.HYPERLIQUID_TESTNET else 'Mainnet'}\n"
- f"๐ Default Symbol: {Config.DEFAULT_TRADING_TOKEN}\n"
- f"๐ฑ Manual trading ready!\n"
- f"๐ Order monitoring: Active ({Config.BOT_HEARTBEAT_SECONDS}s interval)\n"
- f"๐ External trade monitoring: Active\n"
- f"๐ Price alarms: Active\n"
- f"๐ Auto stats sync: Enabled\n"
- f"๐ Logging: {'File + Console' if Config.LOG_TO_FILE else 'Console only'}\n"
- f"โฐ Started at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
- "Use /start for quick actions or /help for all commands."
- )
-
- # Perform initial log cleanup
- try:
- cleanup_logs(days_to_keep=30)
- logger.info("๐งน Initial log cleanup completed")
- except Exception as e:
- logger.warning(f"โ ๏ธ Initial log cleanup failed: {e}")
-
- # Start the application
- await self.application.start()
-
- # Start order monitoring
- await self.start_order_monitoring()
-
- # Start polling for updates manually
- logger.info("๐ Starting update polling...")
-
- # Get updates in a loop
- last_update_id = 0
- while True:
- try:
- # Get updates from Telegram
- updates = await self.application.bot.get_updates(
- offset=last_update_id + 1,
- timeout=30,
- allowed_updates=None
- )
-
- # Process each update
- for update in updates:
- last_update_id = update.update_id
-
- # Process the update through the application
- await self.application.process_update(update)
-
- except Exception as e:
- logger.error(f"Error processing updates: {e}")
- await asyncio.sleep(5) # Wait before retrying
-
- except asyncio.CancelledError:
- logger.info("๐ Bot polling cancelled")
- raise
-
- except Exception as e:
- logger.error(f"โ Error in telegram bot: {e}")
- raise
-
- finally:
- # Clean shutdown
- try:
- await self.stop_order_monitoring()
- if self.application:
- await self.application.stop()
- await self.application.shutdown()
- except Exception as e:
- logger.error(f"Error during shutdown: {e}")
- async def long_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /long command for opening long positions."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("โ Unauthorized access.")
- return
-
- try:
- if not context.args or len(context.args) < 2:
- await update.message.reply_text(
- "โ Usage: /long [token] [USDC amount] [price (optional)]\n"
- "Examples:\n"
- "โข /long BTC 100 - Market order\n"
- "โข /long BTC 100 45000 - Limit order at $45,000"
- )
- return
-
- token = context.args[0].upper()
- usdc_amount = float(context.args[1])
-
- # Check if price is provided for limit order
- limit_price = None
- if len(context.args) >= 3:
- limit_price = float(context.args[2])
- order_type = "Limit"
- order_description = f"at ${limit_price:,.2f}"
- else:
- order_type = "Market"
- order_description = "at current market price"
-
- # Convert token to full symbol format for Hyperliquid
- symbol = f"{token}/USDC:USDC"
-
- # Get current market price to calculate amount and for display
- market_data = self.client.get_market_data(symbol)
- if not market_data:
- await update.message.reply_text(f"โ Could not fetch price for {token}")
- return
-
- current_price = float(market_data['ticker'].get('last', 0))
- if current_price <= 0:
- await update.message.reply_text(f"โ Invalid price for {token}")
- return
-
- # Calculate token amount based on price (market or limit)
- calculation_price = limit_price if limit_price else current_price
- token_amount = usdc_amount / calculation_price
-
- # Create confirmation message
- confirmation_text = f"""
- ๐ข <b>Long Position Confirmation</b>
- ๐ <b>Order Details:</b>
- โข Token: {token}
- โข Direction: LONG (Buy)
- โข USDC Value: ${usdc_amount:,.2f}
- โข Current Price: ${current_price:,.2f}
- โข Order Type: {order_type} Order
- โข Token Amount: {token_amount:.6f} {token}
- ๐ฏ <b>Execution:</b>
- โข Will buy {token_amount:.6f} {token} {order_description}
- โข Est. Value: ${token_amount * calculation_price:,.2f}
- โ ๏ธ <b>Are you sure you want to open this long position?</b>
- """
-
- # Use limit_price for callback if provided, otherwise current_price
- callback_price = limit_price if limit_price else current_price
- callback_data = f"confirm_long_{token}_{usdc_amount}_{callback_price}"
- if limit_price:
- callback_data += "_limit"
-
- keyboard = [
- [
- InlineKeyboardButton("โ
Confirm Long", callback_data=callback_data),
- InlineKeyboardButton("โ Cancel", callback_data="cancel_order")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await update.message.reply_text(confirmation_text, parse_mode='HTML', reply_markup=reply_markup)
-
- except ValueError:
- await update.message.reply_text("โ Invalid USDC amount or price. Please use numbers only.")
- except Exception as e:
- await update.message.reply_text(f"โ Error processing long command: {e}")
-
- async def short_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /short command for opening short positions."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("โ Unauthorized access.")
- return
-
- try:
- if not context.args or len(context.args) < 2:
- await update.message.reply_text(
- "โ Usage: /short [token] [USDC amount] [price (optional)]\n"
- "Examples:\n"
- "โข /short BTC 100 - Market order\n"
- "โข /short BTC 100 46000 - Limit order at $46,000"
- )
- return
-
- token = context.args[0].upper()
- usdc_amount = float(context.args[1])
-
- # Check if price is provided for limit order
- limit_price = None
- if len(context.args) >= 3:
- limit_price = float(context.args[2])
- order_type = "Limit"
- order_description = f"at ${limit_price:,.2f}"
- else:
- order_type = "Market"
- order_description = "at current market price"
-
- # Convert token to full symbol format for Hyperliquid
- symbol = f"{token}/USDC:USDC"
-
- # Get current market price to calculate amount and for display
- market_data = self.client.get_market_data(symbol)
- if not market_data:
- await update.message.reply_text(f"โ Could not fetch price for {token}")
- return
-
- current_price = float(market_data['ticker'].get('last', 0))
- if current_price <= 0:
- await update.message.reply_text(f"โ Invalid price for {token}")
- return
-
- # Calculate token amount based on price (market or limit)
- calculation_price = limit_price if limit_price else current_price
- token_amount = usdc_amount / calculation_price
-
- # Create confirmation message
- confirmation_text = f"""
- ๐ด <b>Short Position Confirmation</b>
- ๐ <b>Order Details:</b>
- โข Token: {token}
- โข Direction: SHORT (Sell)
- โข USDC Value: ${usdc_amount:,.2f}
- โข Current Price: ${current_price:,.2f}
- โข Order Type: {order_type} Order
- โข Token Amount: {token_amount:.6f} {token}
- ๐ฏ <b>Execution:</b>
- โข Will sell {token_amount:.6f} {token} {order_description}
- โข Est. Value: ${token_amount * calculation_price:,.2f}
- โ ๏ธ <b>Are you sure you want to open this short position?</b>
- """
-
- # Use limit_price for callback if provided, otherwise current_price
- callback_price = limit_price if limit_price else current_price
- callback_data = f"confirm_short_{token}_{usdc_amount}_{callback_price}"
- if limit_price:
- callback_data += "_limit"
-
- keyboard = [
- [
- InlineKeyboardButton("โ
Confirm Short", callback_data=callback_data),
- InlineKeyboardButton("โ Cancel", callback_data="cancel_order")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await update.message.reply_text(confirmation_text, parse_mode='HTML', reply_markup=reply_markup)
-
- except ValueError:
- await update.message.reply_text("โ Invalid USDC amount or price. Please use numbers only.")
- except Exception as e:
- await update.message.reply_text(f"โ Error processing short command: {e}")
-
- async def exit_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /exit command for closing positions."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("โ Unauthorized access.")
- return
-
- try:
- if not context.args or len(context.args) < 1:
- await update.message.reply_text(
- "โ Usage: /exit [token]\n"
- "Example: /exit BTC"
- )
- return
-
- token = context.args[0].upper()
- symbol = f"{token}/USDC:USDC"
-
- # Get current positions to find the position for this token
- positions = self.client.get_positions()
- if positions is None:
- await update.message.reply_text(f"โ Could not fetch positions to check {token} position")
- return
-
- # Find the position for this token
- current_position = None
- for position in positions:
- if position.get('symbol') == symbol and float(position.get('contracts', 0)) != 0:
- current_position = position
- break
-
- if not current_position:
- await update.message.reply_text(f"๐ญ No open position found for {token}")
- return
-
- # Extract position details
- contracts = float(current_position.get('contracts', 0))
- entry_price = float(current_position.get('entryPx', 0))
- unrealized_pnl = float(current_position.get('unrealizedPnl', 0))
-
- # Determine position direction and exit details
- if contracts > 0:
- position_type = "LONG"
- exit_side = "sell"
- exit_emoji = "๐ด"
- else:
- position_type = "SHORT"
- exit_side = "buy"
- exit_emoji = "๐ข"
- contracts = abs(contracts) # Make positive for display
-
- # Get current market price
- market_data = self.client.get_market_data(symbol)
- if not market_data:
- await update.message.reply_text(f"โ Could not fetch current price for {token}")
- return
-
- current_price = float(market_data['ticker'].get('last', 0))
- if current_price <= 0:
- await update.message.reply_text(f"โ Invalid current price for {token}")
- return
-
- # Calculate estimated exit value
- exit_value = contracts * current_price
-
- # Create confirmation message
- pnl_emoji = "๐ข" if unrealized_pnl >= 0 else "๐ด"
-
- confirmation_text = f"""
- {exit_emoji} <b>Exit Position Confirmation</b>
- ๐ <b>Position Details:</b>
- โข Token: {token}
- โข Position: {position_type}
- โข Size: {contracts} contracts
- โข Entry Price: ${entry_price:,.2f}
- โข Current Price: ${current_price:,.2f}
- โข {pnl_emoji} Unrealized P&L: ${unrealized_pnl:,.2f}
- ๐ฏ <b>Exit Order:</b>
- โข Action: {exit_side.upper()} (Close {position_type})
- โข Amount: {contracts} {token}
- โข Est. Value: ~${exit_value:,.2f}
- โข Order Type: Market Order
- โ ๏ธ <b>Are you sure you want to close this {position_type} position?</b>
- This will place a market {exit_side} order to close your entire {token} position.
- """
-
- keyboard = [
- [
- InlineKeyboardButton(f"โ
Close {position_type}", callback_data=f"confirm_exit_{token}_{exit_side}_{contracts}_{current_price}"),
- InlineKeyboardButton("โ Cancel", callback_data="cancel_order")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await update.message.reply_text(confirmation_text, parse_mode='HTML', reply_markup=reply_markup)
-
- except ValueError:
- await update.message.reply_text("โ Invalid token format. Please use token symbols like BTC, ETH, etc.")
- except Exception as e:
- await update.message.reply_text(f"โ Error processing exit command: {e}")
- async def coo_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /coo (cancel open orders) command for a specific token."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("โ Unauthorized access.")
- return
-
- try:
- if not context.args or len(context.args) < 1:
- await update.message.reply_text(
- "โ Usage: /coo [token]\n"
- "Example: /coo BTC\n\n"
- "This command cancels ALL open orders for the specified token."
- )
- return
-
- token = context.args[0].upper()
- symbol = f"{token}/USDC:USDC"
-
- # Get current orders for this token
- all_orders = self.client.get_open_orders()
- if all_orders is None:
- await update.message.reply_text(f"โ Could not fetch orders to cancel {token} orders")
- return
-
- # Filter orders for the specific token
- token_orders = [order for order in all_orders if order.get('symbol') == symbol]
-
- if not token_orders:
- await update.message.reply_text(f"๐ญ No open orders found for {token}")
- return
-
- # Create confirmation message with order details
- confirmation_text = f"""
- โ ๏ธ <b>Cancel All {token} Orders</b>
- ๐ <b>Orders to Cancel:</b>
- """
-
- total_value = 0
- for order in token_orders:
- side = order.get('side', 'Unknown')
- amount = order.get('amount', 0)
- price = order.get('price', 0)
- order_id = order.get('id', 'Unknown')
-
- side_emoji = "๐ข" if side.lower() == 'buy' else "๐ด"
- order_value = float(amount) * float(price)
- total_value += order_value
-
- confirmation_text += f"{side_emoji} {side.upper()} {amount} @ ${price:,.2f} (${order_value:,.2f})\n"
-
- confirmation_text += f"""
- ๐ฐ <b>Total Value:</b> ${total_value:,.2f}
- ๐ข <b>Orders Count:</b> {len(token_orders)}
- โ ๏ธ <b>Are you sure you want to cancel ALL {token} orders?</b>
- This action cannot be undone.
- """
-
- keyboard = [
- [
- InlineKeyboardButton(f"โ
Cancel All {token}", callback_data=f"confirm_coo_{token}"),
- InlineKeyboardButton("โ Keep Orders", callback_data="cancel_order")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await update.message.reply_text(confirmation_text, parse_mode='HTML', reply_markup=reply_markup)
-
- except ValueError:
- await update.message.reply_text("โ Invalid token format. Please use token symbols like BTC, ETH, etc.")
- except Exception as e:
- await update.message.reply_text(f"โ Error processing cancel orders command: {e}")
- async def sl_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /sl (stop loss) command for setting stop loss orders."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("โ Unauthorized access.")
- return
-
- try:
- if not context.args or len(context.args) < 2:
- await update.message.reply_text(
- "โ Usage: /sl [token] [price]\n"
- "Example: /sl BTC 44000\n\n"
- "This creates a stop loss order at the specified price."
- )
- return
-
- token = context.args[0].upper()
- stop_price = float(context.args[1])
- symbol = f"{token}/USDC:USDC"
-
- # Get current positions to find the position for this token
- positions = self.client.get_positions()
- if positions is None:
- await update.message.reply_text(f"โ Could not fetch positions to check {token} position")
- return
-
- # Find the position for this token
- current_position = None
- for position in positions:
- if position.get('symbol') == symbol and float(position.get('contracts', 0)) != 0:
- current_position = position
- break
-
- if not current_position:
- await update.message.reply_text(f"๐ญ No open position found for {token}\n\nYou need an open position to set a stop loss.")
- return
-
- # Extract position details
- contracts = float(current_position.get('contracts', 0))
- entry_price = float(current_position.get('entryPx', 0))
- unrealized_pnl = float(current_position.get('unrealizedPnl', 0))
-
- # Determine position direction and validate stop loss price
- if contracts > 0:
- # Long position - stop loss should be below entry price
- position_type = "LONG"
- exit_side = "sell"
- exit_emoji = "๐ด"
- contracts_abs = contracts
-
- if stop_price >= entry_price:
- await update.message.reply_text(
- f"โ ๏ธ Stop loss price should be BELOW entry price for long positions\n\n"
- f"๐ Your {token} LONG position:\n"
- f"โข Entry Price: ${entry_price:,.2f}\n"
- f"โข Stop Price: ${stop_price:,.2f} โ\n\n"
- f"๐ก Try a lower price like: /sl {token} {entry_price * 0.95:.0f}"
- )
- return
- else:
- # Short position - stop loss should be above entry price
- position_type = "SHORT"
- exit_side = "buy"
- exit_emoji = "๐ข"
- contracts_abs = abs(contracts)
-
- if stop_price <= entry_price:
- await update.message.reply_text(
- f"โ ๏ธ Stop loss price should be ABOVE entry price for short positions\n\n"
- f"๐ Your {token} SHORT position:\n"
- f"โข Entry Price: ${entry_price:,.2f}\n"
- f"โข Stop Price: ${stop_price:,.2f} โ\n\n"
- f"๐ก Try a higher price like: /sl {token} {entry_price * 1.05:.0f}"
- )
- return
-
- # Get current market price for reference
- market_data = self.client.get_market_data(symbol)
- current_price = 0
- if market_data:
- current_price = float(market_data['ticker'].get('last', 0))
-
- # Calculate estimated P&L at stop loss
- if contracts > 0: # Long position
- pnl_at_stop = (stop_price - entry_price) * contracts_abs
- else: # Short position
- pnl_at_stop = (entry_price - stop_price) * contracts_abs
-
- # Create confirmation message
- pnl_emoji = "๐ข" if pnl_at_stop >= 0 else "๐ด"
-
- confirmation_text = f"""
- ๐ <b>Stop Loss Order Confirmation</b>
- ๐ <b>Position Details:</b>
- โข Token: {token}
- โข Position: {position_type}
- โข Size: {contracts_abs} contracts
- โข Entry Price: ${entry_price:,.2f}
- โข Current Price: ${current_price:,.2f}
- ๐ฏ <b>Stop Loss Order:</b>
- โข Stop Price: ${stop_price:,.2f}
- โข Action: {exit_side.upper()} (Close {position_type})
- โข Amount: {contracts_abs} {token}
- โข Order Type: Limit Order
- โข {pnl_emoji} Est. P&L: ${pnl_at_stop:,.2f}
- โ ๏ธ <b>Are you sure you want to set this stop loss?</b>
- This will place a limit {exit_side} order at ${stop_price:,.2f} to protect your {position_type} position.
- """
-
- keyboard = [
- [
- InlineKeyboardButton(f"โ
Set Stop Loss", callback_data=f"confirm_sl_{token}_{exit_side}_{contracts_abs}_{stop_price}"),
- InlineKeyboardButton("โ Cancel", callback_data="cancel_order")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await update.message.reply_text(confirmation_text, parse_mode='HTML', reply_markup=reply_markup)
-
- except ValueError:
- await update.message.reply_text("โ Invalid price format. Please use numbers only.")
- except Exception as e:
- await update.message.reply_text(f"โ Error processing stop loss command: {e}")
- async def tp_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /tp (take profit) command for setting take profit orders."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("โ Unauthorized access.")
- return
-
- try:
- if not context.args or len(context.args) < 2:
- await update.message.reply_text(
- "โ Usage: /tp [token] [price]\n"
- "Example: /tp BTC 50000\n\n"
- "This creates a take profit order at the specified price."
- )
- return
-
- token = context.args[0].upper()
- profit_price = float(context.args[1])
- symbol = f"{token}/USDC:USDC"
-
- # Get current positions to find the position for this token
- positions = self.client.get_positions()
- if positions is None:
- await update.message.reply_text(f"โ Could not fetch positions to check {token} position")
- return
-
- # Find the position for this token
- current_position = None
- for position in positions:
- if position.get('symbol') == symbol and float(position.get('contracts', 0)) != 0:
- current_position = position
- break
-
- if not current_position:
- await update.message.reply_text(f"๐ญ No open position found for {token}\n\nYou need an open position to set a take profit.")
- return
-
- # Extract position details
- contracts = float(current_position.get('contracts', 0))
- entry_price = float(current_position.get('entryPx', 0))
- unrealized_pnl = float(current_position.get('unrealizedPnl', 0))
-
- # Determine position direction and validate take profit price
- if contracts > 0:
- # Long position - take profit should be above entry price
- position_type = "LONG"
- exit_side = "sell"
- exit_emoji = "๐ด"
- contracts_abs = contracts
-
- if profit_price <= entry_price:
- await update.message.reply_text(
- f"โ ๏ธ Take profit price should be ABOVE entry price for long positions\n\n"
- f"๐ Your {token} LONG position:\n"
- f"โข Entry Price: ${entry_price:,.2f}\n"
- f"โข Take Profit: ${profit_price:,.2f} โ\n\n"
- f"๐ก Try a higher price like: /tp {token} {entry_price * 1.05:.0f}"
- )
- return
- else:
- # Short position - take profit should be below entry price
- position_type = "SHORT"
- exit_side = "buy"
- exit_emoji = "๐ข"
- contracts_abs = abs(contracts)
-
- if profit_price >= entry_price:
- await update.message.reply_text(
- f"โ ๏ธ Take profit price should be BELOW entry price for short positions\n\n"
- f"๐ Your {token} SHORT position:\n"
- f"โข Entry Price: ${entry_price:,.2f}\n"
- f"โข Take Profit: ${profit_price:,.2f} โ\n\n"
- f"๐ก Try a lower price like: /tp {token} {entry_price * 0.95:.0f}"
- )
- return
-
- # Get current market price for reference
- market_data = self.client.get_market_data(symbol)
- current_price = 0
- if market_data:
- current_price = float(market_data['ticker'].get('last', 0))
-
- # Calculate estimated P&L at take profit
- if contracts > 0: # Long position
- pnl_at_tp = (profit_price - entry_price) * contracts_abs
- else: # Short position
- pnl_at_tp = (entry_price - profit_price) * contracts_abs
-
- # Create confirmation message
- pnl_emoji = "๐ข" if pnl_at_tp >= 0 else "๐ด"
-
- confirmation_text = f"""
- ๐ฏ <b>Take Profit Order Confirmation</b>
- ๐ <b>Position Details:</b>
- โข Token: {token}
- โข Position: {position_type}
- โข Size: {contracts_abs} contracts
- โข Entry Price: ${entry_price:,.2f}
- โข Current Price: ${current_price:,.2f}
- ๐ฐ <b>Take Profit Order:</b>
- โข Target Price: ${profit_price:,.2f}
- โข Action: {exit_side.upper()} (Close {position_type})
- โข Amount: {contracts_abs} {token}
- โข Order Type: Limit Order
- โข {pnl_emoji} Est. P&L: ${pnl_at_tp:,.2f}
- โ ๏ธ <b>Are you sure you want to set this take profit?</b>
- This will place a limit {exit_side} order at ${profit_price:,.2f} to capture profits from your {position_type} position.
- """
-
- keyboard = [
- [
- InlineKeyboardButton(f"โ
Set Take Profit", callback_data=f"confirm_tp_{token}_{exit_side}_{contracts_abs}_{profit_price}"),
- InlineKeyboardButton("โ Cancel", callback_data="cancel_order")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await update.message.reply_text(confirmation_text, parse_mode='HTML', reply_markup=reply_markup)
-
- except ValueError:
- await update.message.reply_text("โ Invalid price format. Please use numbers only.")
- except Exception as e:
- await update.message.reply_text(f"โ Error processing take profit command: {e}")
- async def start_order_monitoring(self):
- """Start the order monitoring background task."""
- if self.monitoring_active:
- return
-
- self.monitoring_active = True
- logger.info("๐ Starting order monitoring...")
-
- # Initialize tracking data
- await self._initialize_order_tracking()
-
- # Start monitoring loop
- asyncio.create_task(self._order_monitoring_loop())
-
- async def stop_order_monitoring(self):
- """Stop the order monitoring background task."""
- self.monitoring_active = False
- logger.info("โน๏ธ Stopping order monitoring...")
-
- async def _initialize_order_tracking(self):
- """Initialize order and position tracking."""
- try:
- # Get current open orders to initialize tracking
- orders = self.client.get_open_orders()
- if orders:
- self.last_known_orders = {order.get('id') for order in orders if order.get('id')}
- logger.info(f"๐ Initialized tracking with {len(self.last_known_orders)} open orders")
-
- # Get current positions for P&L tracking
- positions = self.client.get_positions()
- if positions:
- for position in positions:
- symbol = position.get('symbol')
- contracts = float(position.get('contracts', 0))
- entry_price = float(position.get('entryPx', 0))
-
- if symbol and contracts != 0:
- self.last_known_positions[symbol] = {
- 'contracts': contracts,
- 'entry_price': entry_price
- }
- logger.info(f"๐ Initialized tracking with {len(self.last_known_positions)} positions")
-
- except Exception as e:
- logger.error(f"โ Error initializing order tracking: {e}")
-
- async def _order_monitoring_loop(self):
- """Main monitoring loop that runs every Config.BOT_HEARTBEAT_SECONDS seconds."""
- while self.monitoring_active:
- try:
- await self._check_order_fills()
- await asyncio.sleep(Config.BOT_HEARTBEAT_SECONDS) # Use configurable interval
- except asyncio.CancelledError:
- logger.info("๐ Order monitoring cancelled")
- break
- except Exception as e:
- logger.error(f"โ Error in order monitoring loop: {e}")
- await asyncio.sleep(Config.BOT_HEARTBEAT_SECONDS) # Continue monitoring even if there's an error
-
- async def _check_order_fills(self):
- """Check for filled orders and send notifications."""
- try:
- # Get current orders and positions
- current_orders = self.client.get_open_orders() or []
- current_positions = self.client.get_positions() or []
-
- # Get current order IDs
- current_order_ids = {order.get('id') for order in current_orders if order.get('id')}
-
- # Find filled orders (orders that were in last_known_orders but not in current_orders)
- filled_order_ids = self.last_known_orders - current_order_ids
-
- if filled_order_ids:
- logger.info(f"๐ฏ Detected {len(filled_order_ids)} filled orders")
- await self._process_filled_orders(filled_order_ids, current_positions)
-
- # Update tracking data
- self.last_known_orders = current_order_ids
- await self._update_position_tracking(current_positions)
-
- # Check price alarms
- await self._check_price_alarms()
-
- # Check external trades (trades made outside the bot)
- await self._check_external_trades()
-
- # Check stop losses (if risk management is enabled)
- if Config.RISK_MANAGEMENT_ENABLED:
- await self._check_stop_losses(current_positions)
-
- except Exception as e:
- logger.error(f"โ Error checking order fills: {e}")
- async def _check_price_alarms(self):
- """Check all active price alarms."""
- try:
- # Get all active alarms
- active_alarms = self.alarm_manager.get_all_active_alarms()
- if not active_alarms:
- return
-
- # Get unique tokens from alarms
- tokens_to_check = list(set(alarm['token'] for alarm in active_alarms))
-
- # Fetch current prices for all tokens
- price_data = {}
- for token in tokens_to_check:
- symbol = f"{token}/USDC:USDC"
- market_data = self.client.get_market_data(symbol)
-
- if market_data and market_data.get('ticker'):
- current_price = market_data['ticker'].get('last')
- if current_price is not None:
- price_data[token] = float(current_price)
-
- # Check alarms against current prices
- triggered_alarms = self.alarm_manager.check_alarms(price_data)
-
- # Send notifications for triggered alarms
- for alarm in triggered_alarms:
- await self._send_alarm_notification(alarm)
-
- except Exception as e:
- logger.error(f"โ Error checking price alarms: {e}")
- async def _send_alarm_notification(self, alarm: Dict[str, Any]):
- """Send notification for triggered alarm."""
- try:
- message = self.alarm_manager.format_triggered_alarm(alarm)
- await self.send_message(message)
- logger.info(f"๐ข Sent alarm notification: {alarm['token']} ID {alarm['id']} @ ${alarm['triggered_price']}")
- except Exception as e:
- logger.error(f"โ Error sending alarm notification: {e}")
- async def _check_external_trades(self):
- """Check for trades made outside the Telegram bot and update stats."""
- try:
- # Get recent fills from Hyperliquid
- recent_fills = self.client.get_recent_fills()
-
- if not recent_fills:
- return
-
- # Initialize last processed time if first run
- if self.last_processed_trade_time is None:
- # Set to current time minus 1 hour to catch recent activity
- self.last_processed_trade_time = (datetime.now() - timedelta(hours=1)).isoformat()
-
- # Filter for new trades since last check
- new_trades = []
- latest_trade_time = self.last_processed_trade_time
-
- for fill in recent_fills:
- fill_time = fill.get('timestamp')
- if fill_time:
- # Convert timestamps to comparable format
- try:
- # Convert fill_time to string if it's not already
- if isinstance(fill_time, (int, float)):
- # Assume it's a unix timestamp
- fill_time_str = datetime.fromtimestamp(fill_time / 1000 if fill_time > 1e10 else fill_time).isoformat()
- else:
- fill_time_str = str(fill_time)
-
- # Compare as strings
- if fill_time_str > self.last_processed_trade_time:
- new_trades.append(fill)
- if fill_time_str > latest_trade_time:
- latest_trade_time = fill_time_str
- except Exception as timestamp_error:
- logger.warning(f"โ ๏ธ Error processing timestamp {fill_time}: {timestamp_error}")
- continue
-
- if not new_trades:
- return
-
- # Process new trades
- for trade in new_trades:
- await self._process_external_trade(trade)
-
- # Update last processed time
- self.last_processed_trade_time = latest_trade_time
-
- if new_trades:
- logger.info(f"๐ Processed {len(new_trades)} external trades")
-
- except Exception as e:
- logger.error(f"โ Error checking external trades: {e}")
- async def _process_external_trade(self, trade: Dict[str, Any]):
- """Process an individual external trade."""
- try:
- # Extract trade information
- symbol = trade.get('symbol', '')
- side = trade.get('side', '')
- amount = float(trade.get('amount', 0))
- price = float(trade.get('price', 0))
- trade_id = trade.get('id', 'external')
- timestamp = trade.get('timestamp', '')
-
- if not all([symbol, side, amount, price]):
- return
-
- # Record trade in stats
- self.stats.record_trade(symbol, side, amount, price, trade_id)
-
- # Send notification for significant trades
- await self._send_external_trade_notification(trade)
-
- logger.info(f"๐ Processed external trade: {side} {amount} {symbol} @ ${price}")
-
- except Exception as e:
- logger.error(f"โ Error processing external trade: {e}")
- async def _send_external_trade_notification(self, trade: Dict[str, Any]):
- """Send notification for external trades."""
- try:
- symbol = trade.get('symbol', '')
- side = trade.get('side', '')
- amount = float(trade.get('amount', 0))
- price = float(trade.get('price', 0))
- timestamp = trade.get('timestamp', '')
-
- # Extract token from symbol
- token = symbol.split('/')[0] if '/' in symbol else symbol
-
- # Format timestamp
- try:
- trade_time = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
- time_str = trade_time.strftime('%H:%M:%S')
- except:
- time_str = "Unknown"
-
- # Determine trade type and emoji
- side_emoji = "๐ข" if side.lower() == 'buy' else "๐ด"
- trade_value = amount * price
-
- message = f"""
- ๐ <b>External Trade Detected</b>
- ๐ <b>Trade Details:</b>
- โข Token: {token}
- โข Side: {side.upper()}
- โข Amount: {amount} {token}
- โข Price: ${price:,.2f}
- โข Value: ${trade_value:,.2f}
- {side_emoji} <b>Source:</b> Direct Platform Trade
- โฐ <b>Time:</b> {time_str}
- ๐ <b>Note:</b> This trade was executed outside the Telegram bot
- ๐ Stats have been automatically updated
- """
-
- await self.send_message(message.strip())
- logger.info(f"๐ข Sent external trade notification: {side} {amount} {token}")
-
- except Exception as e:
- logger.error(f"โ Error sending external trade notification: {e}")
- async def _check_stop_losses(self, current_positions: list):
- """Check all positions for stop loss triggers and execute automatic exits."""
- try:
- if not current_positions:
- return
-
- stop_loss_triggers = []
-
- for position in current_positions:
- symbol = position.get('symbol')
- contracts = float(position.get('contracts', 0))
- entry_price = float(position.get('entryPx', 0))
-
- if not symbol or contracts == 0 or entry_price == 0:
- continue
-
- # Get current market price
- market_data = self.client.get_market_data(symbol)
- if not market_data or not market_data.get('ticker'):
- continue
-
- current_price = float(market_data['ticker'].get('last', 0))
- if current_price == 0:
- continue
-
- # Calculate current P&L percentage
- if contracts > 0: # Long position
- pnl_percent = ((current_price - entry_price) / entry_price) * 100
- else: # Short position
- pnl_percent = ((entry_price - current_price) / entry_price) * 100
-
- # Check if stop loss should trigger
- if pnl_percent <= -Config.STOP_LOSS_PERCENTAGE:
- token = symbol.split('/')[0] if '/' in symbol else symbol
- stop_loss_triggers.append({
- 'symbol': symbol,
- 'token': token,
- 'contracts': contracts,
- 'entry_price': entry_price,
- 'current_price': current_price,
- 'pnl_percent': pnl_percent
- })
-
- # Execute stop losses
- for trigger in stop_loss_triggers:
- await self._execute_automatic_stop_loss(trigger)
-
- except Exception as e:
- logger.error(f"โ Error checking stop losses: {e}")
- async def _execute_automatic_stop_loss(self, trigger: Dict[str, Any]):
- """Execute an automatic stop loss order."""
- try:
- symbol = trigger['symbol']
- token = trigger['token']
- contracts = trigger['contracts']
- entry_price = trigger['entry_price']
- current_price = trigger['current_price']
- pnl_percent = trigger['pnl_percent']
-
- # Determine the exit side (opposite of position)
- exit_side = 'sell' if contracts > 0 else 'buy'
- contracts_abs = abs(contracts)
-
- # Send notification before executing
- await self._send_stop_loss_notification(trigger, "triggered")
-
- # Execute the stop loss order (market order for immediate execution)
- try:
- if exit_side == 'sell':
- order = self.client.create_market_sell_order(symbol, contracts_abs)
- else:
- order = self.client.create_market_buy_order(symbol, contracts_abs)
-
- if order:
- logger.info(f"๐ Stop loss executed: {token} {exit_side} {contracts_abs} @ ${current_price}")
-
- # Record the trade in stats
- self.stats.record_trade(symbol, exit_side, contracts_abs, current_price, order.get('id', 'stop_loss'))
-
- # Send success notification
- await self._send_stop_loss_notification(trigger, "executed", order)
- else:
- logger.error(f"โ Stop loss order failed for {token}")
- await self._send_stop_loss_notification(trigger, "failed")
-
- except Exception as order_error:
- logger.error(f"โ Stop loss order execution failed for {token}: {order_error}")
- await self._send_stop_loss_notification(trigger, "failed", error=str(order_error))
-
- except Exception as e:
- logger.error(f"โ Error executing automatic stop loss: {e}")
- async def _send_stop_loss_notification(self, trigger: Dict[str, Any], status: str, order: Dict = None, error: str = None):
- """Send notification for stop loss events."""
- try:
- token = trigger['token']
- contracts = trigger['contracts']
- entry_price = trigger['entry_price']
- current_price = trigger['current_price']
- pnl_percent = trigger['pnl_percent']
-
- position_type = "LONG" if contracts > 0 else "SHORT"
- contracts_abs = abs(contracts)
-
- if status == "triggered":
- title = "๐ Stop Loss Triggered"
- status_text = f"Stop loss triggered at {Config.STOP_LOSS_PERCENTAGE}% loss"
- emoji = "๐จ"
- elif status == "executed":
- title = "โ
Stop Loss Executed"
- status_text = "Position closed automatically"
- emoji = "๐"
- elif status == "failed":
- title = "โ Stop Loss Failed"
- status_text = f"Stop loss execution failed{': ' + error if error else ''}"
- emoji = "โ ๏ธ"
- else:
- return
-
- # Calculate loss
- loss_value = contracts_abs * abs(current_price - entry_price)
-
- message = f"""
- {title}
- {emoji} <b>Risk Management Alert</b>
- ๐ <b>Position Details:</b>
- โข Token: {token}
- โข Direction: {position_type}
- โข Size: {contracts_abs} contracts
- โข Entry Price: ${entry_price:,.2f}
- โข Current Price: ${current_price:,.2f}
- ๐ด <b>Loss Details:</b>
- โข Loss: ${loss_value:,.2f} ({pnl_percent:.2f}%)
- โข Stop Loss Threshold: {Config.STOP_LOSS_PERCENTAGE}%
- ๐ <b>Action:</b> {status_text}
- โฐ <b>Time:</b> {datetime.now().strftime('%H:%M:%S')}
- """
-
- if order and status == "executed":
- order_id = order.get('id', 'N/A')
- message += f"\n๐ <b>Order ID:</b> {order_id}"
-
- await self.send_message(message.strip())
- logger.info(f"๐ข Sent stop loss notification: {token} {status}")
-
- except Exception as e:
- logger.error(f"โ Error sending stop loss notification: {e}")
- async def _process_filled_orders(self, filled_order_ids: set, current_positions: list):
- """Process filled orders and determine if they opened or closed positions."""
- try:
- # Create a map of current positions
- current_position_map = {}
- for position in current_positions:
- symbol = position.get('symbol')
- contracts = float(position.get('contracts', 0))
- if symbol:
- current_position_map[symbol] = contracts
-
- # For each symbol, check if position size changed
- for symbol, old_position_data in self.last_known_positions.items():
- old_contracts = old_position_data['contracts']
- current_contracts = current_position_map.get(symbol, 0)
-
- if old_contracts != current_contracts:
- # Position changed - determine if it's open or close
- await self._handle_position_change(symbol, old_position_data, current_contracts)
-
- # Check for new positions (symbols not in last_known_positions)
- for symbol, current_contracts in current_position_map.items():
- if symbol not in self.last_known_positions and current_contracts != 0:
- # New position opened
- await self._handle_new_position(symbol, current_contracts)
-
- except Exception as e:
- logger.error(f"โ Error processing filled orders: {e}")
-
- async def _handle_position_change(self, symbol: str, old_position_data: dict, current_contracts: float):
- """Handle when an existing position changes size."""
- old_contracts = old_position_data['contracts']
- old_entry_price = old_position_data['entry_price']
-
- # Get current market price
- market_data = self.client.get_market_data(symbol)
- current_price = 0
- if market_data:
- current_price = float(market_data['ticker'].get('last', 0))
-
- token = symbol.split('/')[0] if '/' in symbol else symbol
-
- if current_contracts == 0 and old_contracts != 0:
- # Position closed
- await self._send_close_trade_notification(token, old_contracts, old_entry_price, current_price)
- elif abs(current_contracts) > abs(old_contracts):
- # Position increased
- added_contracts = current_contracts - old_contracts
- await self._send_open_trade_notification(token, added_contracts, current_price, "increased")
- elif abs(current_contracts) < abs(old_contracts):
- # Position decreased (partial close)
- closed_contracts = old_contracts - current_contracts
- await self._send_partial_close_notification(token, closed_contracts, old_entry_price, current_price)
-
- async def _handle_new_position(self, symbol: str, contracts: float):
- """Handle when a new position is opened."""
- # Get current market price
- market_data = self.client.get_market_data(symbol)
- current_price = 0
- if market_data:
- current_price = float(market_data['ticker'].get('last', 0))
-
- token = symbol.split('/')[0] if '/' in symbol else symbol
- await self._send_open_trade_notification(token, contracts, current_price, "opened")
-
- async def _update_position_tracking(self, current_positions: list):
- """Update the position tracking data."""
- new_position_map = {}
-
- for position in current_positions:
- symbol = position.get('symbol')
- contracts = float(position.get('contracts', 0))
- entry_price = float(position.get('entryPx', 0))
-
- if symbol and contracts != 0:
- new_position_map[symbol] = {
- 'contracts': contracts,
- 'entry_price': entry_price
- }
-
- self.last_known_positions = new_position_map
-
- async def _send_open_trade_notification(self, token: str, contracts: float, price: float, action: str):
- """Send notification for opened/increased position."""
- position_type = "LONG" if contracts > 0 else "SHORT"
- contracts_abs = abs(contracts)
- value = contracts_abs * price
-
- if action == "opened":
- title = "๐ Position Opened"
- action_text = f"New {position_type} position opened"
- else:
- title = "๐ Position Increased"
- action_text = f"{position_type} position increased"
-
- message = f"""
- {title}
- ๐ <b>Trade Details:</b>
- โข Token: {token}
- โข Direction: {position_type}
- โข Size: {contracts_abs} contracts
- โข Entry Price: ${price:,.2f}
- โข Value: ${value:,.2f}
- โ
<b>Status:</b> {action_text}
- โฐ <b>Time:</b> {datetime.now().strftime('%H:%M:%S')}
- ๐ฑ Use /positions to view all positions
- """
-
- await self.send_message(message.strip())
- logger.info(f"๐ข Sent open trade notification: {token} {position_type} {contracts_abs} @ ${price}")
-
- async def _send_close_trade_notification(self, token: str, contracts: float, entry_price: float, exit_price: float):
- """Send notification for closed position with P&L."""
- position_type = "LONG" if contracts > 0 else "SHORT"
- contracts_abs = abs(contracts)
-
- # Calculate P&L
- if contracts > 0: # Long position
- pnl = (exit_price - entry_price) * contracts_abs
- else: # Short position
- pnl = (entry_price - exit_price) * contracts_abs
-
- pnl_percent = (pnl / (entry_price * contracts_abs)) * 100 if entry_price > 0 else 0
- pnl_emoji = "๐ข" if pnl >= 0 else "๐ด"
-
- exit_value = contracts_abs * exit_price
-
- message = f"""
- ๐ฏ <b>Position Closed</b>
- ๐ <b>Trade Summary:</b>
- โข Token: {token}
- โข Direction: {position_type}
- โข Size: {contracts_abs} contracts
- โข Entry Price: ${entry_price:,.2f}
- โข Exit Price: ${exit_price:,.2f}
- โข Exit Value: ${exit_value:,.2f}
- {pnl_emoji} <b>Profit & Loss:</b>
- โข P&L: ${pnl:,.2f} ({pnl_percent:+.2f}%)
- โข Result: {"PROFIT" if pnl >= 0 else "LOSS"}
- โ
<b>Status:</b> Position fully closed
- โฐ <b>Time:</b> {datetime.now().strftime('%H:%M:%S')}
- ๐ Use /stats to view updated performance
- """
-
- await self.send_message(message.strip())
- logger.info(f"๐ข Sent close trade notification: {token} {position_type} P&L: ${pnl:.2f}")
-
- async def _send_partial_close_notification(self, token: str, contracts: float, entry_price: float, exit_price: float):
- """Send notification for partially closed position."""
- position_type = "LONG" if contracts > 0 else "SHORT"
- contracts_abs = abs(contracts)
-
- # Calculate P&L for closed portion
- if contracts > 0: # Long position
- pnl = (exit_price - entry_price) * contracts_abs
- else: # Short position
- pnl = (entry_price - exit_price) * contracts_abs
-
- pnl_percent = (pnl / (entry_price * contracts_abs)) * 100 if entry_price > 0 else 0
- pnl_emoji = "๐ข" if pnl >= 0 else "๐ด"
-
- message = f"""
- ๐ <b>Position Partially Closed</b>
- ๐ <b>Partial Close Details:</b>
- โข Token: {token}
- โข Direction: {position_type}
- โข Closed Size: {contracts_abs} contracts
- โข Entry Price: ${entry_price:,.2f}
- โข Exit Price: ${exit_price:,.2f}
- {pnl_emoji} <b>Partial P&L:</b>
- โข P&L: ${pnl:,.2f} ({pnl_percent:+.2f}%)
- โ
<b>Status:</b> Partial position closed
- โฐ <b>Time:</b> {datetime.now().strftime('%H:%M:%S')}
- ๐ Use /positions to view remaining position
- """
-
- await self.send_message(message.strip())
- logger.info(f"๐ข Sent partial close notification: {token} {position_type} Partial P&L: ${pnl:.2f}")
- async def monitoring_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /monitoring command to show monitoring status."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("โ Unauthorized access.")
- return
-
- # Get alarm statistics
- alarm_stats = self.alarm_manager.get_statistics()
-
- status_text = f"""
- ๐ <b>System Monitoring Status</b>
- ๐ <b>Order Monitoring:</b>
- โข Active: {'โ
Yes' if self.monitoring_active else 'โ No'}
- โข Check Interval: {Config.BOT_HEARTBEAT_SECONDS} seconds
- โข Orders Tracked: {len(self.last_known_orders)}
- โข Positions Tracked: {len(self.last_known_positions)}
- ๐ <b>Price Alarms:</b>
- โข Active Alarms: {alarm_stats['total_active']}
- โข Triggered Today: {alarm_stats['total_triggered']}
- โข Tokens Monitored: {alarm_stats['tokens_tracked']}
- โข Next Alarm ID: {alarm_stats['next_id']}
- ๐ <b>External Trade Monitoring:</b>
- โข Last Check: {self.last_processed_trade_time or 'Not started'}
- โข Auto Stats Update: โ
Enabled
- โข External Notifications: โ
Enabled
- ๐ก๏ธ <b>Risk Management:</b>
- โข Automatic Stop Loss: {'โ
Enabled' if Config.RISK_MANAGEMENT_ENABLED else 'โ Disabled'}
- โข Stop Loss Threshold: {Config.STOP_LOSS_PERCENTAGE}%
- โข Position Monitoring: {'โ
Active' if Config.RISK_MANAGEMENT_ENABLED else 'โ Inactive'}
- ๐ <b>Notifications:</b>
- โข ๐ Position Opened/Increased
- โข ๐ Position Partially/Fully Closed
- โข ๐ฏ P&L Calculations
- โข ๐ Price Alarm Triggers
- โข ๐ External Trade Detection
- โข ๐ Automatic Stop Loss Triggers
- โฐ <b>Last Check:</b> {datetime.now().strftime('%H:%M:%S')}
- ๐ก <b>Monitoring Features:</b>
- โข Real-time order fill detection
- โข Automatic P&L calculation
- โข Position change tracking
- โข Price alarm monitoring
- โข External trade monitoring
- โข Auto stats synchronization
- โข Instant Telegram notifications
- """
-
- if alarm_stats['token_breakdown']:
- status_text += f"\n\n๐ <b>Active Alarms by Token:</b>\n"
- for token, count in alarm_stats['token_breakdown'].items():
- status_text += f"โข {token}: {count} alarm{'s' if count != 1 else ''}\n"
-
- await update.message.reply_text(status_text.strip(), parse_mode='HTML')
- async def alarm_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /alarm command for price alerts."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("โ Unauthorized access.")
- return
-
- try:
- if not context.args or len(context.args) == 0:
- # No arguments - list all alarms
- alarms = self.alarm_manager.get_all_active_alarms()
- message = self.alarm_manager.format_alarm_list(alarms)
- await update.message.reply_text(message, parse_mode='HTML')
- return
-
- elif len(context.args) == 1:
- arg = context.args[0]
-
- # Check if argument is a number (alarm ID to remove)
- try:
- alarm_id = int(arg)
- # Remove alarm by ID
- if self.alarm_manager.remove_alarm(alarm_id):
- await update.message.reply_text(f"โ
Alarm ID {alarm_id} has been removed.")
- else:
- await update.message.reply_text(f"โ Alarm ID {alarm_id} not found.")
- return
- except ValueError:
- # Not a number, treat as token
- token = arg.upper()
- alarms = self.alarm_manager.get_alarms_by_token(token)
- message = self.alarm_manager.format_alarm_list(alarms, f"{token} Price Alarms")
- await update.message.reply_text(message, parse_mode='HTML')
- return
-
- elif len(context.args) == 2:
- # Set new alarm: /alarm TOKEN PRICE
- token = context.args[0].upper()
- target_price = float(context.args[1])
-
- # Get current market price
- symbol = f"{token}/USDC:USDC"
- market_data = self.client.get_market_data(symbol)
-
- if not market_data or not market_data.get('ticker'):
- await update.message.reply_text(f"โ Could not fetch current price for {token}")
- return
-
- current_price = float(market_data['ticker'].get('last', 0))
- if current_price <= 0:
- await update.message.reply_text(f"โ Invalid current price for {token}")
- return
-
- # Create the alarm
- alarm = self.alarm_manager.create_alarm(token, target_price, current_price)
-
- # Format confirmation message
- direction_emoji = "๐" if alarm['direction'] == 'above' else "๐"
- price_diff = abs(target_price - current_price)
- price_diff_percent = (price_diff / current_price) * 100
-
- message = f"""
- โ
<b>Price Alarm Created</b>
- ๐ <b>Alarm Details:</b>
- โข Alarm ID: {alarm['id']}
- โข Token: {token}
- โข Target Price: ${target_price:,.2f}
- โข Current Price: ${current_price:,.2f}
- โข Direction: {alarm['direction'].upper()}
- {direction_emoji} <b>Alert Condition:</b>
- Will trigger when {token} price moves {alarm['direction']} ${target_price:,.2f}
- ๐ฐ <b>Price Difference:</b>
- โข Distance: ${price_diff:,.2f} ({price_diff_percent:.2f}%)
- โข Status: ACTIVE โ
- โฐ <b>Created:</b> {datetime.now().strftime('%H:%M:%S')}
- ๐ก The alarm will be checked every {Config.BOT_HEARTBEAT_SECONDS} seconds and you'll receive a notification when triggered.
- """
-
- await update.message.reply_text(message.strip(), parse_mode='HTML')
-
- else:
- # Too many arguments
- await update.message.reply_text(
- "โ Invalid usage. Examples:\n\n"
- "โข <code>/alarm</code> - List all alarms\n"
- "โข <code>/alarm BTC</code> - List BTC alarms\n"
- "โข <code>/alarm BTC 50000</code> - Set alarm for BTC at $50,000\n"
- "โข <code>/alarm 3</code> - Remove alarm ID 3",
- parse_mode='HTML'
- )
-
- except ValueError:
- await update.message.reply_text("โ Invalid price format. Please use numbers only.")
- except Exception as e:
- error_message = f"โ Error processing alarm command: {str(e)}"
- await update.message.reply_text(error_message)
- logger.error(f"Error in alarm command: {e}")
- async def logs_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /logs command to show log file statistics and cleanup options."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("โ Unauthorized access.")
- return
-
- try:
- # Check for cleanup argument
- if context.args and len(context.args) >= 1:
- if context.args[0].lower() == 'cleanup':
- # Get days parameter (default 30)
- days_to_keep = 30
- if len(context.args) >= 2:
- try:
- days_to_keep = int(context.args[1])
- except ValueError:
- await update.message.reply_text("โ Invalid number of days. Using default (30).")
-
- # Perform cleanup
- await update.message.reply_text(f"๐งน Cleaning up log files older than {days_to_keep} days...")
- cleanup_logs(days_to_keep)
- await update.message.reply_text(f"โ
Log cleanup completed!")
- return
-
- # Show log statistics
- log_stats_text = format_log_stats()
-
- # Add additional info
- status_text = f"""
- ๐ <b>System Logging Status</b>
- {log_stats_text}
- ๐ <b>Log Configuration:</b>
- โข Log Level: {Config.LOG_LEVEL}
- โข Heartbeat Interval: {Config.BOT_HEARTBEAT_SECONDS}s
- โข Bot Uptime: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
- ๐ก <b>Log Management:</b>
- โข <code>/logs cleanup</code> - Clean old logs (30 days)
- โข <code>/logs cleanup 7</code> - Clean logs older than 7 days
- โข Log rotation happens automatically
- โข Old backups are removed automatically
- ๐ง <b>Configuration:</b>
- โข Rotation Type: {Config.LOG_ROTATION_TYPE}
- โข Max Size: {Config.LOG_MAX_SIZE_MB}MB (size rotation)
- โข Backup Count: {Config.LOG_BACKUP_COUNT}
- """
-
- await update.message.reply_text(status_text.strip(), parse_mode='HTML')
-
- except Exception as e:
- error_message = f"โ Error processing logs command: {str(e)}"
- await update.message.reply_text(error_message)
- logger.error(f"Error in logs command: {e}")
- async def performance_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /performance command to show token performance ranking or detailed stats."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_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(update, token)
- else:
- # Show token performance ranking
- await self._show_performance_ranking(update)
-
- except Exception as e:
- error_message = f"โ Error processing performance command: {str(e)}"
- await update.message.reply_text(error_message)
- logger.error(f"Error in performance command: {e}")
- async def _show_performance_ranking(self, update: Update):
- """Show token performance ranking (compressed view)."""
- token_performance = self.stats.get_token_performance()
-
- if not token_performance:
- await update.message.reply_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) 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['total_pnl'] >= 0 else "๐ด"
-
- # Format the line
- performance_text += f"{rank_emoji} <b>{token}</b>\n"
- performance_text += f" {pnl_emoji} P&L: ${stats['total_pnl']:,.2f} ({stats['pnl_percentage']:+.1f}%)\n"
- performance_text += f" ๐ Trades: {stats['completed_trades']}"
-
- # Add win rate if there are completed trades
- if stats['completed_trades'] > 0:
- performance_text += f" | Win: {stats['win_rate']:.0f}%"
-
- performance_text += "\n\n"
-
- # Add summary
- total_pnl = sum(stats['total_pnl'] for stats in token_performance.values())
- total_trades = sum(stats['completed_trades'] for stats 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 update.message.reply_text(performance_text.strip(), parse_mode='HTML')
- async def _show_token_performance(self, update: Update, token: str):
- """Show detailed performance for a specific token."""
- token_stats = self.stats.get_token_detailed_stats(token)
-
- # Check if token has any data
- if token_stats.get('total_trades', 0) == 0:
- await update.message.reply_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 update.message.reply_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 update.message.reply_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."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("โ Unauthorized access.")
- return
-
- try:
- daily_stats = self.stats.get_daily_stats(10)
-
- if not daily_stats:
- await update.message.reply_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
-
- for day_stats in daily_stats:
- 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']
-
- # Add summary
- total_pnl_emoji = "๐ข" if total_pnl >= 0 else "๐ด"
- daily_text += f"๐ผ <b>10-Day Summary:</b>\n"
- daily_text += f" {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
- daily_text += f" ๐ Total Trades: {total_trades}\n"
- daily_text += f" ๐ Avg per Day: ${total_pnl/len(daily_stats):,.2f}"
-
- await update.message.reply_text(daily_text.strip(), parse_mode='HTML')
-
- except Exception as e:
- error_message = f"โ Error processing daily command: {str(e)}"
- await update.message.reply_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."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("โ Unauthorized access.")
- return
-
- try:
- weekly_stats = self.stats.get_weekly_stats(10)
-
- if not weekly_stats:
- await update.message.reply_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
-
- for week_stats in weekly_stats:
- 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']
-
- # Add summary
- 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" ๐ Avg per Week: ${total_pnl/len(weekly_stats):,.2f}"
-
- await update.message.reply_text(weekly_text.strip(), parse_mode='HTML')
-
- except Exception as e:
- error_message = f"โ Error processing weekly command: {str(e)}"
- await update.message.reply_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."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("โ Unauthorized access.")
- return
-
- try:
- monthly_stats = self.stats.get_monthly_stats(10)
-
- if not monthly_stats:
- await update.message.reply_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
-
- for month_stats in monthly_stats:
- 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']
-
- # Add summary
- 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" ๐ Avg per Month: ${total_pnl/len(monthly_stats):,.2f}"
-
- await update.message.reply_text(monthly_text.strip(), parse_mode='HTML')
-
- except Exception as e:
- error_message = f"โ Error processing monthly command: {str(e)}"
- await update.message.reply_text(error_message)
- logger.error(f"Error in monthly command: {e}")
- async def main_async():
- """Async main entry point for the Telegram bot."""
- try:
- # Validate configuration
- if not Config.validate():
- logger.error("โ Configuration validation failed!")
- return
-
- if not Config.TELEGRAM_ENABLED:
- logger.error("โ Telegram is not enabled in configuration")
- return
-
- # Create and run the bot
- bot = TelegramTradingBot()
- await bot.run()
-
- except KeyboardInterrupt:
- logger.info("๐ Bot stopped by user")
- except Exception as e:
- logger.error(f"โ Unexpected error: {e}")
- raise
- def main():
- """Main entry point for the Telegram bot."""
- try:
- # Check if we're already in an asyncio context
- try:
- loop = asyncio.get_running_loop()
- # If we get here, we're already in an asyncio context
- logger.error("โ Cannot run main() from within an asyncio context. Use main_async() instead.")
- return
- except RuntimeError:
- # No running loop, safe to use asyncio.run()
- pass
-
- # Run the async main function
- asyncio.run(main_async())
-
- except Exception as e:
- logger.error(f"โ Failed to start telegram bot: {e}")
- raise
- if __name__ == "__main__":
- main()
|