123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395 |
- #!/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
- import json
- import os
- from datetime import datetime, timedelta
- from typing import Optional, Dict, Any, List, Tuple
- from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, ReplyKeyboardMarkup, KeyboardButton
- 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 trading bot for manual trading operations."""
-
- def __init__(self):
- """Initialize the Telegram trading bot."""
- self.client = HyperliquidClient()
- self.application = None
- self.order_monitoring_task = None
- self.last_filled_orders = set()
- self.alarms = [] # List to store price alarms
- self.bot_heartbeat_seconds = getattr(Config, 'BOT_HEARTBEAT_SECONDS', 10)
- self.external_trade_timestamps = set() # Track external trade timestamps to avoid duplicates
- self.last_position_check = {} # Track last position state for comparison
- self.position_tracker = {} # For enhanced position tracking
- self.stats = None
- self.version = "Unknown" # Will be set by launcher
-
- # Bot state persistence file
- self.bot_state_file = "bot_state.json"
-
- # Order monitoring attributes
- 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
-
- # Deposit/Withdrawal monitoring
- self.last_deposit_withdrawal_check = None # Track last deposit/withdrawal check
- self.deposit_withdrawal_check_interval = 3600 # Check every hour (3600 seconds)
-
- # Alarm management
- self.alarm_manager = AlarmManager()
-
- # Pending stop loss storage
- self.pending_stop_losses = {} # Format: {order_id: {'token': str, 'stop_price': float, 'side': str, 'amount': float, 'order_type': str}}
-
- # Load bot state first, then initialize stats
- self._load_bot_state()
- self._initialize_stats()
-
- def _load_bot_state(self):
- """Load bot state from disk."""
- try:
- if os.path.exists(self.bot_state_file):
- with open(self.bot_state_file, 'r') as f:
- state_data = json.load(f)
-
- # Restore critical state
- self.pending_stop_losses = state_data.get('pending_stop_losses', {})
- self.last_known_orders = set(state_data.get('last_known_orders', []))
- self.last_known_positions = state_data.get('last_known_positions', {})
-
- # Restore timestamps (convert from ISO string if present)
- last_trade_time = state_data.get('last_processed_trade_time')
- if last_trade_time:
- try:
- self.last_processed_trade_time = datetime.fromisoformat(last_trade_time)
- except (ValueError, TypeError):
- self.last_processed_trade_time = None
-
- last_deposit_check = state_data.get('last_deposit_withdrawal_check')
- if last_deposit_check:
- try:
- self.last_deposit_withdrawal_check = datetime.fromisoformat(last_deposit_check)
- except (ValueError, TypeError):
- self.last_deposit_withdrawal_check = None
-
- logger.info(f"🔄 Restored bot state: {len(self.pending_stop_losses)} pending stop losses, {len(self.last_known_orders)} tracked orders")
-
- # Log details about restored pending stop losses
- if self.pending_stop_losses:
- for order_id, stop_loss_info in self.pending_stop_losses.items():
- token = stop_loss_info.get('token', 'Unknown')
- stop_price = stop_loss_info.get('stop_price', 0)
- order_type = stop_loss_info.get('order_type', 'Unknown')
- logger.info(f"📋 Restored pending stop loss: {order_id} -> {token} {order_type} @ ${stop_price}")
-
- except Exception as e:
- logger.error(f"❌ Error loading bot state: {e}")
- # Initialize with defaults
- self.pending_stop_losses = {}
- self.last_known_orders = set()
- self.last_known_positions = {}
- self.last_processed_trade_time = None
- self.last_deposit_withdrawal_check = None
-
- def _save_bot_state(self):
- """Save bot state to disk."""
- try:
- state_data = {
- 'pending_stop_losses': self.pending_stop_losses,
- 'last_known_orders': list(self.last_known_orders), # Convert set to list for JSON
- 'last_known_positions': self.last_known_positions,
- 'last_processed_trade_time': self.last_processed_trade_time.isoformat() if self.last_processed_trade_time else None,
- 'last_deposit_withdrawal_check': self.last_deposit_withdrawal_check.isoformat() if self.last_deposit_withdrawal_check else None,
- 'last_updated': datetime.now().isoformat(),
- 'version': self.version
- }
-
- with open(self.bot_state_file, 'w') as f:
- json.dump(state_data, f, indent=2, default=str)
-
- logger.debug(f"💾 Saved bot state: {len(self.pending_stop_losses)} pending stop losses")
-
- except Exception as e:
- logger.error(f"❌ Error saving bot state: {e}")
- def _initialize_stats(self):
- """Initialize stats with current balance."""
- try:
- # Initialize TradingStats object first
- self.stats = TradingStats()
-
- # Get current balance and set it as initial balance
- 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(Config.TELEGRAM_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 Config.TELEGRAM_CHAT_ID:
- try:
- await self.application.bot.send_message(
- chat_id=Config.TELEGRAM_CHAT_ID,
- text=text,
- parse_mode=parse_mode
- )
- except Exception as e:
- logger.error(f"Failed to send message: {e}")
-
- def _create_custom_keyboard(self) -> Optional[ReplyKeyboardMarkup]:
- """Create a custom keyboard from configuration."""
- if not Config.TELEGRAM_CUSTOM_KEYBOARD_ENABLED:
- return None
-
- try:
- layout = Config.TELEGRAM_CUSTOM_KEYBOARD_LAYOUT
- # Parse the layout: "cmd1,cmd2,cmd3|cmd4,cmd5|cmd6,cmd7,cmd8,cmd9"
- rows = layout.split('|')
- keyboard = []
-
- for row in rows:
- commands = [cmd.strip() for cmd in row.split(',') if cmd.strip()]
- if commands:
- keyboard.append([KeyboardButton(cmd.lstrip('/').capitalize()) for cmd in commands])
-
- if keyboard:
- return ReplyKeyboardMarkup(
- keyboard,
- resize_keyboard=True, # Resize to fit screen
- one_time_keyboard=False, # Keep keyboard persistent
- selective=True # Show only to authorized users
- )
- except Exception as e:
- logger.error(f"Failed to create custom keyboard: {e}")
-
- return None
-
- 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>Welcome to Hyperliquid Trading Bot</b>
- 📱 <b>Quick Actions:</b>
- • Trading: /long BTC 100 or /short ETH 50
- • Exit: /exit BTC (closes position)
- • Info: /balance, /positions, /orders
- 📊 <b>Market Data:</b>
- • /market - Detailed market overview
- • /price - Quick price check
- <b>⚡ Quick Commands:</b>
- • /balance - Account balance
- • /positions - Open positions
- • /orders - Active orders
- • /market - Market data & prices
- <b>🚀 Trading:</b>
- • /long BTC 100 - Long position
- • /long BTC 100 45000 - Limit order
- • /long BTC 100 sl:44000 - With stop loss
- • /short ETH 50 - Short position
- • /short ETH 50 3500 sl:3600 - With stop loss
- • /exit BTC - Close position
- • /coo BTC - Cancel open orders
- <b>🛡️ Risk Management:</b>
- • Enabled: {risk_enabled}
- • Auto Stop Loss: {stop_loss}%
- • Order Stop Loss: Use sl:price parameter
- • /sl BTC 44000 - Manual stop loss
- • /tp BTC 50000 - Take profit order
- <b>📈 Performance & 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
- • /version - Bot version & system information
- • /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>🔄 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
- • Automatic stop loss placement
- • {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
- • Automatic stop loss protection
- <b>📱 Mobile Optimized:</b>
- • Quick action buttons
- • Instant notifications
- • Clean, readable layout
- • One-tap commands
- <b>💡 Quick Access:</b>
- • /commands or /c - One-tap button menu for all commands
- • Buttons below for instant access to key functions
- 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)
-
- # Create custom keyboard for persistent buttons
- custom_keyboard = self._create_custom_keyboard()
-
- # Send message with inline keyboard
- await update.message.reply_text(
- welcome_text,
- parse_mode='HTML',
- reply_markup=reply_markup
- )
-
- # If custom keyboard is enabled, send a follow-up message to set the custom keyboard
- if custom_keyboard:
- await update.message.reply_text(
- "⌨️ <b>Custom keyboard enabled!</b>\n\nUse the buttons below for quick access to commands:",
- parse_mode='HTML',
- reply_markup=custom_keyboard
- )
-
- 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
- • /balance_adjustments - View deposit/withdrawal history
- <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)
- • /long BTC 100 sl:44000 - Market order with automatic stop loss
- • /long BTC 100 45000 sl:44000 - Limit order with automatic stop loss
- • /short ETH 50 - Short ETH with $50 USDC (Market Order)
- • /short ETH 50 3500 - Short ETH with $50 USDC at $3,500 (Limit Order)
- • /short ETH 50 sl:3600 - Market order with automatic stop loss
- • /short ETH 50 3500 sl:3600 - Limit order with automatic stop loss
- • /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
- • Order-based: Use sl:price parameter for automatic placement
- <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
- • /version - Bot version & system information
- • /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
- • Deposit/withdrawal tracking (hourly)
- • Risk metrics calculation
- • Automatic stop loss placement
- <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 commands_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /commands and /c command with quick action buttons."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("❌ Unauthorized access.")
- return
-
- commands_text = """
- 📱 <b>Quick Commands</b>
- Tap any button below for instant access to bot functions:
- 💡 <b>Pro Tip:</b> These buttons work the same as typing the commands manually, but faster!
- """
-
- keyboard = [
- [
- InlineKeyboardButton("💰 Balance", callback_data="balance"),
- InlineKeyboardButton("📈 Positions", callback_data="positions")
- ],
- [
- InlineKeyboardButton("📋 Orders", callback_data="orders"),
- InlineKeyboardButton("📊 Stats", callback_data="stats")
- ],
- [
- InlineKeyboardButton("💵 Price", callback_data="price"),
- InlineKeyboardButton("📊 Market", callback_data="market")
- ],
- [
- InlineKeyboardButton("🏆 Performance", callback_data="performance"),
- InlineKeyboardButton("🔔 Alarms", callback_data="alarm")
- ],
- [
- InlineKeyboardButton("📅 Daily", callback_data="daily"),
- InlineKeyboardButton("📊 Weekly", callback_data="weekly")
- ],
- [
- InlineKeyboardButton("📆 Monthly", callback_data="monthly"),
- InlineKeyboardButton("🔄 Trades", callback_data="trades")
- ],
- [
- InlineKeyboardButton("🔄 Monitoring", callback_data="monitoring"),
- InlineKeyboardButton("📝 Logs", callback_data="logs")
- ],
- [
- InlineKeyboardButton("⚙️ Help", callback_data="help")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await update.message.reply_text(commands_text, parse_mode='HTML', reply_markup=reply_markup)
-
- 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"
-
- # Debug: Show raw balance structure (can be removed after debugging)
- logger.debug(f"Raw balance data: {balance}")
-
- # CCXT balance structure includes 'free', 'used', and 'total'
- total_balance = balance.get('total', {})
- free_balance = balance.get('free', {})
- used_balance = balance.get('used', {})
-
- 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 (convert all to USDC equivalent for summary)
- if asset == 'USDC':
- total_value += float(amount)
- available_value += free_amount
- else:
- # For non-USDC assets, add to totals (assuming 1:1 for now, could be enhanced with price conversion)
- 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:
- # Use USDC balance for P&L calculation
- usdc_total = float(total_balance.get('USDC', 0))
- pnl = usdc_total - basic_stats['initial_balance']
- pnl_percent = (pnl / basic_stats['initial_balance']) * 100
-
- balance_text += f"\n📊 <b>Performance (USDC):</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 /long or /short 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()
-
- # Debug: Log what we got from orders
- logger.debug(f"Raw orders data: {orders}")
- logger.debug(f"Orders type: {type(orders)}, Length: {len(orders) if orders else 'None'}")
-
- 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])
- try:
- price = float(parts[4])
- except (ValueError, TypeError):
- price = None # Will be handled in execute_long_order
- is_limit = len(parts) > 5 and parts[5] == 'limit'
-
- # Parse stop loss if present
- stop_loss_price = None
- if len(parts) > 6 and parts[6] == 'sl':
- try:
- stop_loss_price = float(parts[7])
- except (ValueError, TypeError):
- stop_loss_price = None
- elif len(parts) > 5 and parts[5] == 'sl':
- try:
- stop_loss_price = float(parts[6])
- except (ValueError, TypeError):
- stop_loss_price = None
-
- await self._execute_long_order(query, token, usdc_amount, price, is_limit, stop_loss_price)
- return
-
- elif callback_data.startswith('confirm_short_'):
- parts = callback_data.split('_')
- token = parts[2]
- usdc_amount = float(parts[3])
- try:
- price = float(parts[4])
- except (ValueError, TypeError):
- price = None # Will be handled in execute_short_order
- is_limit = len(parts) > 5 and parts[5] == 'limit'
-
- # Parse stop loss if present
- stop_loss_price = None
- if len(parts) > 6 and parts[6] == 'sl':
- try:
- stop_loss_price = float(parts[7])
- except (ValueError, TypeError):
- stop_loss_price = None
- elif len(parts) > 5 and parts[5] == 'sl':
- try:
- stop_loss_price = float(parts[6])
- except (ValueError, TypeError):
- stop_loss_price = None
-
- await self._execute_short_order(query, token, usdc_amount, price, is_limit, stop_loss_price)
- 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)
- elif callback_data == "performance":
- await self.performance_command(fake_update, context)
- elif callback_data == "alarm":
- await self.alarm_command(fake_update, context)
- elif callback_data == "daily":
- await self.daily_command(fake_update, context)
- elif callback_data == "weekly":
- await self.weekly_command(fake_update, context)
- elif callback_data == "monthly":
- await self.monthly_command(fake_update, context)
- elif callback_data == "monitoring":
- await self.monitoring_command(fake_update, context)
- elif callback_data == "logs":
- await self.logs_command(fake_update, context)
-
- async def _execute_long_order(self, query, token: str, usdc_amount: float, price: float, is_limit: bool, stop_loss_price: float = None):
- """Execute a long order."""
- symbol = f"{token}/USDC:USDC"
-
- try:
- await query.edit_message_text("⏳ Opening long position...")
-
- # Validate price
- if price is None or price <= 0:
- # Try to get current market price
- market_data = self.client.get_market_data(symbol)
- if market_data and market_data.get('ticker'):
- price = float(market_data['ticker'].get('last', 0))
- if price <= 0:
- await query.edit_message_text("❌ Unable to get valid market price. Please try again.")
- return
- else:
- await query.edit_message_text("❌ Unable to fetch market price. Please try again.")
- return
-
- # 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
- if actual_price is None or actual_price <= 0:
- # This should not happen due to our price validation above, but extra safety
- actual_price = price
- action_type = self.stats.record_trade_with_enhanced_tracking(symbol, 'buy', token_amount, actual_price, order_id, "bot")
-
- # Save pending stop loss if provided
- if stop_loss_price is not None:
- self.pending_stop_losses[order_id] = {
- 'token': token,
- 'symbol': symbol,
- 'stop_price': stop_loss_price,
- 'side': 'sell', # For long position, stop loss is a sell order
- 'amount': token_amount,
- 'order_type': 'long',
- 'original_order_id': order_id,
- 'is_limit': is_limit
- }
- self._save_bot_state() # Save state after adding pending stop loss
- logger.info(f"💾 Saved pending stop loss for order {order_id}: sell {token_amount:.6f} {token} @ ${stop_loss_price}")
-
- 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>
- """
-
- # Add stop loss confirmation if provided
- if stop_loss_price is not None:
- success_message += f"""
- 🛑 <b>Stop Loss Saved:</b>
- • Stop Price: ${stop_loss_price:,.2f}
- • Will be placed automatically when order fills
- • Status: PENDING ⏳
- """
-
- success_message += f"\n🚀 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, stop_loss_price: float = None):
- """Execute a short order."""
- symbol = f"{token}/USDC:USDC"
-
- try:
- await query.edit_message_text("⏳ Opening short position...")
-
- # Validate price
- if price is None or price <= 0:
- # Try to get current market price
- market_data = self.client.get_market_data(symbol)
- if market_data and market_data.get('ticker'):
- price = float(market_data['ticker'].get('last', 0))
- if price <= 0:
- await query.edit_message_text("❌ Unable to get valid market price. Please try again.")
- return
- else:
- await query.edit_message_text("❌ Unable to fetch market price. Please try again.")
- return
-
- # 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
- if actual_price is None or actual_price <= 0:
- # This should not happen due to our price validation above, but extra safety
- actual_price = price
- action_type = self.stats.record_trade_with_enhanced_tracking(symbol, 'sell', token_amount, actual_price, order_id, "bot")
-
- # Save pending stop loss if provided
- if stop_loss_price is not None:
- self.pending_stop_losses[order_id] = {
- 'token': token,
- 'symbol': symbol,
- 'stop_price': stop_loss_price,
- 'side': 'buy', # For short position, stop loss is a buy order
- 'amount': token_amount,
- 'order_type': 'short',
- 'original_order_id': order_id,
- 'is_limit': is_limit
- }
- self._save_bot_state() # Save state after adding pending stop loss
- logger.info(f"💾 Saved pending stop loss for order {order_id}: buy {token_amount:.6f} {token} @ ${stop_loss_price}")
-
- 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>
- """
-
- # Add stop loss confirmation if provided
- if stop_loss_price is not None:
- success_message += f"""
- 🛑 <b>Stop Loss Saved:</b>
- • Stop Price: ${stop_loss_price:,.2f}
- • Will be placed automatically when order fills
- • Status: PENDING ⏳
- """
-
- success_message += f"\n📉 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
- if actual_price is None or actual_price <= 0:
- # Fallback to ensure we have a valid price
- actual_price = price
- action_type = self.stats.record_trade_with_enhanced_tracking(symbol, exit_side, contracts, actual_price, order_id, "bot")
-
- 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
- action_type = self.stats.record_trade_with_enhanced_tracking(symbol, exit_side, contracts, actual_price, order_id, "bot")
-
- 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
- if actual_price is None or actual_price <= 0:
- # Fallback to ensure we have a valid price
- actual_price = price
- action_type = self.stats.record_trade_with_enhanced_tracking(symbol, exit_side, contracts, actual_price, order_id, "bot")
-
- 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 handle_keyboard_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle messages from custom keyboard buttons (without /)."""
- if not self.is_authorized(update.effective_chat.id):
- return
-
- message_text = update.message.text.lower()
-
- # Map clean button text to command handlers
- command_handlers = {
- 'daily': self.daily_command,
- 'performance': self.performance_command,
- 'balance': self.balance_command,
- 'stats': self.stats_command,
- 'positions': self.positions_command,
- 'orders': self.orders_command,
- 'price': self.price_command,
- 'market': self.market_command,
- 'help': self.help_command,
- 'commands': self.commands_command
- }
-
- # Execute the corresponding command handler
- if message_text in command_handlers:
- await command_handlers[message_text](update, context)
- 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("commands", self.commands_command))
- self.application.add_handler(CommandHandler("c", self.commands_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))
- self.application.add_handler(CommandHandler("risk", self.risk_command))
- self.application.add_handler(CommandHandler("version", self.version_command))
- self.application.add_handler(CommandHandler("balance_adjustments", self.balance_adjustments_command))
- self.application.add_handler(CommandHandler("keyboard", self.keyboard_command))
-
- # Callback query handler for inline keyboards
- self.application.add_handler(CallbackQueryHandler(self.button_callback))
-
- # Handle clean keyboard button messages (without /)
- self.application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, self.handle_keyboard_message))
-
- # 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(
- f"🤖 <b>Manual Trading Bot v{self.version} 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)] [sl:price (optional)]\n"
- "Examples:\n"
- "• /long BTC 100 - Market order\n"
- "• /long BTC 100 45000 - Limit order at $45,000\n"
- "• /long BTC 100 sl:44000 - Market order with stop loss at $44,000\n"
- "• /long BTC 100 45000 sl:44000 - Limit order at $45,000 with stop loss at $44,000"
- )
- return
-
- token = context.args[0].upper()
- usdc_amount = float(context.args[1])
-
- # Parse arguments for price and stop loss
- limit_price = None
- stop_loss_price = None
-
- # Parse remaining arguments
- for i, arg in enumerate(context.args[2:], 2):
- if arg.startswith('sl:'):
- # Stop loss parameter
- try:
- stop_loss_price = float(arg[3:]) # Remove 'sl:' prefix
- except ValueError:
- await update.message.reply_text("❌ Invalid stop loss price format. Use sl:price (e.g., sl:44000)")
- return
- elif limit_price is None:
- # First non-sl parameter is the limit price
- try:
- limit_price = float(arg)
- except ValueError:
- await update.message.reply_text("❌ Invalid limit price format. Please use numbers only.")
- return
-
- # Determine order type
- if limit_price:
- 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
-
- # Validate stop loss price for long positions
- if stop_loss_price is not None:
- entry_price = limit_price if limit_price else current_price
- if stop_loss_price >= entry_price:
- await update.message.reply_text(
- f"❌ Stop loss price should be BELOW entry price for long positions\n\n"
- f"📊 Entry Price: ${entry_price:,.2f}\n"
- f"🛑 Stop Loss: ${stop_loss_price:,.2f} ❌\n\n"
- f"💡 Try a lower price like: /long {token} {usdc_amount} {f'{limit_price} ' if limit_price else ''}sl:{entry_price * 0.95:.0f}"
- )
- 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}
- """
-
- # Add stop loss information if provided
- if stop_loss_price is not None:
- confirmation_text += f"""
- 🛑 <b>Stop Loss:</b>
- • Stop Price: ${stop_loss_price:,.2f}
- • Will be placed automatically after order fills
- • Protection Level: {((calculation_price - stop_loss_price) / calculation_price * 100):.1f}% below entry
- """
-
- confirmation_text += "\n⚠️ <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"
- if stop_loss_price is not None:
- callback_data += f"_sl_{stop_loss_price}"
-
- 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)] [sl:price (optional)]\n"
- "Examples:\n"
- "• /short BTC 100 - Market order\n"
- "• /short BTC 100 46000 - Limit order at $46,000\n"
- "• /short BTC 100 sl:47000 - Market order with stop loss at $47,000\n"
- "• /short BTC 100 46000 sl:47000 - Limit order at $46,000 with stop loss at $47,000"
- )
- return
-
- token = context.args[0].upper()
- usdc_amount = float(context.args[1])
-
- # Parse arguments for price and stop loss
- limit_price = None
- stop_loss_price = None
-
- # Parse remaining arguments
- for i, arg in enumerate(context.args[2:], 2):
- if arg.startswith('sl:'):
- # Stop loss parameter
- try:
- stop_loss_price = float(arg[3:]) # Remove 'sl:' prefix
- except ValueError:
- await update.message.reply_text("❌ Invalid stop loss price format. Use sl:price (e.g., sl:47000)")
- return
- elif limit_price is None:
- # First non-sl parameter is the limit price
- try:
- limit_price = float(arg)
- except ValueError:
- await update.message.reply_text("❌ Invalid limit price format. Please use numbers only.")
- return
-
- # Determine order type
- if limit_price:
- 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
-
- # Validate stop loss price for short positions
- if stop_loss_price is not None:
- entry_price = limit_price if limit_price else current_price
- if stop_loss_price <= entry_price:
- await update.message.reply_text(
- f"❌ Stop loss price should be ABOVE entry price for short positions\n\n"
- f"📊 Entry Price: ${entry_price:,.2f}\n"
- f"🛑 Stop Loss: ${stop_loss_price:,.2f} ❌\n\n"
- f"💡 Try a higher price like: /short {token} {usdc_amount} {f'{limit_price} ' if limit_price else ''}sl:{entry_price * 1.05:.0f}"
- )
- 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}
- """
-
- # Add stop loss information if provided
- if stop_loss_price is not None:
- confirmation_text += f"""
- 🛑 <b>Stop Loss:</b>
- • Stop Price: ${stop_loss_price:,.2f}
- • Will be placed automatically after order fills
- • Protection Level: {((stop_loss_price - calculation_price) / calculation_price * 100):.1f}% above entry
- """
-
- confirmation_text += "\n⚠️ <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"
- if stop_loss_price is not None:
- callback_data += f"_sl_{stop_loss_price}"
-
- 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."""
- # Safety check in case this is called before initialization is complete
- if not hasattr(self, 'monitoring_active'):
- self.monitoring_active = False
-
- 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."""
- # Safety check in case this is called before initialization is complete
- if hasattr(self, 'monitoring_active'):
- 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 getattr(self, 'monitoring_active', False):
- 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)
-
- # Process pending stop losses for filled orders
- await self._process_pending_stop_losses(filled_order_ids)
-
- # Update tracking data
- self.last_known_orders = current_order_ids
- await self._update_position_tracking(current_positions)
-
- # Save state after updating tracking data
- self._save_bot_state()
-
- # 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)
-
- # Check deposits/withdrawals (hourly)
- await self._check_deposits_withdrawals()
-
- # Clean up cancelled orders from pending stop losses
- await self._cleanup_cancelled_stop_losses(current_order_ids)
-
- except Exception as e:
- logger.error(f"❌ Error checking order fills: {e}")
- async def _process_pending_stop_losses(self, filled_order_ids: set):
- """Process pending stop losses for filled orders."""
- try:
- processed_any = False
- for order_id in filled_order_ids:
- if order_id in self.pending_stop_losses:
- stop_loss_info = self.pending_stop_losses[order_id]
-
- # Place the stop loss order
- await self._place_pending_stop_loss(order_id, stop_loss_info)
-
- # Remove from pending after processing
- del self.pending_stop_losses[order_id]
- processed_any = True
- logger.info(f"🗑️ Removed processed stop loss for order {order_id}")
-
- # Save state if any stop losses were processed
- if processed_any:
- self._save_bot_state()
-
- except Exception as e:
- logger.error(f"❌ Error processing pending stop losses: {e}")
- async def _place_pending_stop_loss(self, original_order_id: str, stop_loss_info: Dict[str, Any]):
- """Place a pending stop loss order."""
- try:
- token = stop_loss_info['token']
- symbol = stop_loss_info['symbol']
- stop_price = stop_loss_info['stop_price']
- side = stop_loss_info['side']
- amount = stop_loss_info['amount']
- order_type = stop_loss_info['order_type']
-
- logger.info(f"🛑 Placing automatic stop loss: {side} {amount:.6f} {token} @ ${stop_price}")
-
- # Place the stop loss order as a limit order
- order = self.client.place_limit_order(symbol, side, amount, stop_price)
-
- if order:
- order_id = order.get('id', 'N/A')
-
- # Send notification
- await self._send_stop_loss_placed_notification(token, order_type, stop_price, amount, order_id, original_order_id)
-
- logger.info(f"✅ Successfully placed automatic stop loss for {token}: Order ID {order_id}")
- else:
- # Send failure notification
- await self._send_stop_loss_failed_notification(token, order_type, stop_price, amount, original_order_id)
- logger.error(f"❌ Failed to place automatic stop loss for {token}")
-
- except Exception as e:
- logger.error(f"❌ Error placing pending stop loss: {e}")
- await self._send_stop_loss_failed_notification(
- stop_loss_info.get('token', 'Unknown'),
- stop_loss_info.get('order_type', 'Unknown'),
- stop_loss_info.get('stop_price', 0),
- stop_loss_info.get('amount', 0),
- original_order_id,
- str(e)
- )
- async def _cleanup_cancelled_stop_losses(self, current_order_ids: set):
- """Remove pending stop losses for cancelled orders."""
- try:
- # Find orders that are no longer active but were not filled
- orders_to_remove = []
-
- for order_id, stop_loss_info in self.pending_stop_losses.items():
- if order_id not in current_order_ids:
- # Order is no longer in open orders, check if it was cancelled (not filled)
- # We assume if it's not in current_order_ids and we haven't processed it as filled,
- # then it was likely cancelled
- orders_to_remove.append(order_id)
-
- # Remove cancelled orders from pending stop losses
- for order_id in orders_to_remove:
- stop_loss_info = self.pending_stop_losses[order_id]
- token = stop_loss_info['token']
-
- # Send notification about cancelled stop loss
- await self._send_stop_loss_cancelled_notification(token, stop_loss_info, order_id)
-
- # Remove from pending
- del self.pending_stop_losses[order_id]
- logger.info(f"🗑️ Removed pending stop loss for cancelled order {order_id}")
-
- # Save state if any stop losses were removed
- if orders_to_remove:
- self._save_bot_state()
-
- except Exception as e:
- logger.error(f"❌ Error cleaning up cancelled stop losses: {e}")
- async def _send_stop_loss_placed_notification(self, token: str, order_type: str, stop_price: float, amount: float, stop_order_id: str, original_order_id: str):
- """Send notification when stop loss is successfully placed."""
- try:
- position_type = order_type.upper()
-
- message = f"""
- 🛑 <b>Stop Loss Placed Automatically</b>
- ✅ <b>Stop Loss Active</b>
- 📊 <b>Details:</b>
- • Token: {token}
- • Position: {position_type}
- • Stop Price: ${stop_price:,.2f}
- • Amount: {amount:.6f} {token}
- • Stop Loss Order ID: <code>{stop_order_id}</code>
- • Original Order ID: <code>{original_order_id}</code>
- 🎯 <b>Protection:</b>
- • Status: ACTIVE ✅
- • Will execute if price reaches ${stop_price:,.2f}
- • Order Type: Limit Order
- 💡 Your {position_type} position is now protected with automatic stop loss!
- """
-
- await self.send_message(message.strip())
- logger.info(f"📢 Sent stop loss placed notification: {token} @ ${stop_price}")
-
- except Exception as e:
- logger.error(f"❌ Error sending stop loss placed notification: {e}")
- async def _send_stop_loss_failed_notification(self, token: str, order_type: str, stop_price: float, amount: float, original_order_id: str, error: str = None):
- """Send notification when stop loss placement fails."""
- try:
- position_type = order_type.upper()
-
- message = f"""
- ⚠️ <b>Stop Loss Placement Failed</b>
- ❌ <b>Automatic Stop Loss Failed</b>
- 📊 <b>Details:</b>
- • Token: {token}
- • Position: {position_type}
- • Intended Stop Price: ${stop_price:,.2f}
- • Amount: {amount:.6f} {token}
- • Original Order ID: <code>{original_order_id}</code>
- 🚨 <b>Action Required:</b>
- • Your position is NOT protected
- • Consider manually setting stop loss: <code>/sl {token} {stop_price:.0f}</code>
- • Monitor your position closely
- {f'🔧 Error: {error}' if error else ''}
- 💡 Use /sl command to manually set stop loss protection.
- """
-
- await self.send_message(message.strip())
- logger.info(f"📢 Sent stop loss failed notification: {token}")
-
- except Exception as e:
- logger.error(f"❌ Error sending stop loss failed notification: {e}")
- async def _send_stop_loss_cancelled_notification(self, token: str, stop_loss_info: Dict[str, Any], order_id: str):
- """Send notification when stop loss is cancelled due to order cancellation."""
- try:
- position_type = stop_loss_info['order_type'].upper()
- stop_price = stop_loss_info['stop_price']
-
- message = f"""
- 🚫 <b>Stop Loss Cancelled</b>
- 📊 <b>Original Order Cancelled</b>
- • Token: {token}
- • Position: {position_type}
- • Cancelled Stop Price: ${stop_price:,.2f}
- • Original Order ID: <code>{order_id}</code>
- 💡 <b>Status:</b>
- • Pending stop loss automatically cancelled
- • No position protection was placed
- • Order was cancelled before execution
- 🔄 If you still want to trade {token}, place a new order with stop loss protection.
- """
-
- await self.send_message(message.strip())
- logger.info(f"📢 Sent stop loss cancelled notification: {token}")
-
- except Exception as e:
- logger.error(f"❌ Error sending stop loss cancelled notification: {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
-
- # Save state after updating last processed time
- self._save_bot_state()
-
- 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 _check_deposits_withdrawals(self):
- """Check for deposits and withdrawals to maintain accurate P&L tracking."""
- try:
- # Check if it's time to run (hourly check)
- current_time = datetime.now()
-
- if self.last_deposit_withdrawal_check is not None:
- time_since_last_check = (current_time - self.last_deposit_withdrawal_check).total_seconds()
- if time_since_last_check < self.deposit_withdrawal_check_interval:
- return # Not time to check yet
-
- logger.info("🔍 Checking for deposits and withdrawals...")
-
- # Initialize last check time if first run
- if self.last_deposit_withdrawal_check is None:
- # Set to 24 hours ago to catch recent activity
- self.last_deposit_withdrawal_check = current_time - timedelta(hours=24)
-
- # Calculate timestamp for API calls (last check time)
- since_timestamp = int(self.last_deposit_withdrawal_check.timestamp() * 1000) # Hyperliquid expects milliseconds
-
- # Track new deposits/withdrawals
- new_deposits = 0
- new_withdrawals = 0
-
- # Check if sync_client is available
- if not hasattr(self.client, 'sync_client') or not self.client.sync_client:
- logger.warning("⚠️ CCXT sync_client not available for deposit/withdrawal checking")
- self.last_deposit_withdrawal_check = current_time
- return
-
- # Set up user parameter for Hyperliquid API calls
- params = {}
- if Config.HYPERLIQUID_WALLET_ADDRESS:
- wallet_address = Config.HYPERLIQUID_WALLET_ADDRESS
- params['user'] = f"0x{wallet_address}" if not wallet_address.startswith('0x') else wallet_address
- else:
- logger.warning("⚠️ No wallet address configured for deposit/withdrawal checking")
- self.last_deposit_withdrawal_check = current_time
- return
-
- # Check for deposits
- try:
- deposits = self.client.sync_client.fetch_deposits(code='USDC', since=since_timestamp, params=params)
- if deposits:
- for deposit in deposits:
- amount = float(deposit.get('amount', 0))
- timestamp = deposit.get('datetime', datetime.now().isoformat())
- deposit_id = deposit.get('id', 'unknown')
-
- # Record in stats to adjust P&L calculations
- self.stats.record_deposit(amount, timestamp, deposit_id)
- new_deposits += 1
-
- # Send notification
- await self._send_deposit_notification(amount, timestamp)
-
- except Exception as e:
- logger.warning(f"⚠️ Error fetching deposits: {e}")
-
- # Check for withdrawals
- try:
- withdrawals = self.client.sync_client.fetch_withdrawals(code='USDC', since=since_timestamp, params=params)
- if withdrawals:
- for withdrawal in withdrawals:
- amount = float(withdrawal.get('amount', 0))
- timestamp = withdrawal.get('datetime', datetime.now().isoformat())
- withdrawal_id = withdrawal.get('id', 'unknown')
-
- # Record in stats to adjust P&L calculations
- self.stats.record_withdrawal(amount, timestamp, withdrawal_id)
- new_withdrawals += 1
-
- # Send notification
- await self._send_withdrawal_notification(amount, timestamp)
-
- except Exception as e:
- logger.warning(f"⚠️ Error fetching withdrawals: {e}")
-
- # Update last check time
- self.last_deposit_withdrawal_check = current_time
-
- # Save state after updating last check time
- self._save_bot_state()
-
- if new_deposits > 0 or new_withdrawals > 0:
- logger.info(f"💰 Processed {new_deposits} deposits and {new_withdrawals} withdrawals")
-
- # Get updated balance adjustments summary
- adjustments = self.stats.get_balance_adjustments_summary()
- logger.info(f"📊 Total adjustments: ${adjustments['net_adjustment']:,.2f} net ({adjustments['adjustment_count']} total)")
-
- except Exception as e:
- logger.error(f"❌ Error checking deposits/withdrawals: {e}")
- async def _send_deposit_notification(self, amount: float, timestamp: str):
- """Send notification for detected deposit."""
- try:
- time_str = datetime.fromisoformat(timestamp.replace('Z', '+00:00')).strftime('%H:%M:%S')
-
- message = f"""
- 💰 <b>Deposit Detected</b>
- 💵 <b>Amount:</b> ${amount:,.2f} USDC
- ⏰ <b>Time:</b> {time_str}
- 📊 <b>P&L Impact:</b>
- • Initial balance adjusted to maintain accurate P&L
- • Trading statistics unaffected by balance change
- • This deposit will not show as trading profit
- ✅ <b>Balance tracking updated automatically</b>
- """
-
- await self.send_message(message.strip())
- logger.info(f"📱 Sent deposit notification: ${amount:,.2f}")
-
- except Exception as e:
- logger.error(f"❌ Error sending deposit notification: {e}")
- async def _send_withdrawal_notification(self, amount: float, timestamp: str):
- """Send notification for detected withdrawal."""
- try:
- time_str = datetime.fromisoformat(timestamp.replace('Z', '+00:00')).strftime('%H:%M:%S')
-
- message = f"""
- 💸 <b>Withdrawal Detected</b>
- 💵 <b>Amount:</b> ${amount:,.2f} USDC
- ⏰ <b>Time:</b> {time_str}
- 📊 <b>P&L Impact:</b>
- • Initial balance adjusted to maintain accurate P&L
- • Trading statistics unaffected by balance change
- • This withdrawal will not show as trading loss
- ✅ <b>Balance tracking updated automatically</b>
- """
-
- await self.send_message(message.strip())
- logger.info(f"📱 Sent withdrawal notification: ${amount:,.2f}")
-
- except Exception as e:
- logger.error(f"❌ Error sending withdrawal notification: {e}")
- async def _process_external_trade(self, trade: Dict[str, Any]):
- """Process an individual external trade and determine if it's opening or closing a position."""
- 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 and get action type using enhanced tracking
- action_type = self.stats.record_trade_with_enhanced_tracking(symbol, side, amount, price, trade_id, "external")
-
- # Send enhanced notification based on action type
- await self._send_enhanced_trade_notification(symbol, side, amount, price, action_type, timestamp)
-
- logger.info(f"📋 Processed external trade: {side} {amount} {symbol} @ ${price} ({action_type})")
-
- except Exception as e:
- logger.error(f"❌ Error processing external trade: {e}")
- async def _send_enhanced_trade_notification(self, symbol: str, side: str, amount: float, price: float, action_type: str, timestamp: str = None):
- """Send enhanced trade notification based on position action type."""
- try:
- token = symbol.split('/')[0] if '/' in symbol else symbol
- position = self.stats.get_enhanced_position_state(symbol)
-
- if timestamp is None:
- time_str = datetime.now().strftime('%H:%M:%S')
- else:
- try:
- time_obj = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
- time_str = time_obj.strftime('%H:%M:%S')
- except:
- time_str = "Unknown"
-
- # Handle different action types
- if action_type in ['long_opened', 'short_opened']:
- await self._send_position_opened_notification(token, side, amount, price, action_type, time_str)
-
- elif action_type in ['long_increased', 'short_increased']:
- await self._send_position_increased_notification(token, side, amount, price, position, action_type, time_str)
-
- elif action_type in ['long_reduced', 'short_reduced']:
- pnl_data = self.stats.calculate_enhanced_position_pnl(symbol, amount, price)
- await self._send_position_reduced_notification(token, side, amount, price, position, pnl_data, action_type, time_str)
-
- elif action_type in ['long_closed', 'short_closed']:
- pnl_data = self.stats.calculate_enhanced_position_pnl(symbol, amount, price)
- await self._send_position_closed_notification(token, side, amount, price, position, pnl_data, action_type, time_str)
-
- elif action_type in ['long_closed_and_short_opened', 'short_closed_and_long_opened']:
- await self._send_position_flipped_notification(token, side, amount, price, action_type, time_str)
-
- else:
- # Fallback to generic notification
- await self._send_external_trade_notification({
- 'symbol': symbol,
- 'side': side,
- 'amount': amount,
- 'price': price,
- 'timestamp': timestamp or datetime.now().isoformat()
- })
-
- except Exception as e:
- logger.error(f"❌ Error sending enhanced trade notification: {e}")
- async def _send_position_opened_notification(self, token: str, side: str, amount: float, price: float, action_type: str, time_str: str):
- """Send notification for newly opened position."""
- position_type = "LONG" if action_type == 'long_opened' else "SHORT"
- side_emoji = "🟢" if side.lower() == 'buy' else "🔴"
- trade_value = amount * price
-
- message = f"""
- 🚀 <b>Position Opened</b>
- 📊 <b>New {position_type} Position:</b>
- • Token: {token}
- • Direction: {position_type}
- • Entry Size: {amount} {token}
- • Entry Price: ${price:,.2f}
- • Position Value: ${trade_value:,.2f}
- {side_emoji} <b>Trade Details:</b>
- • Side: {side.upper()}
- • Order Type: Market/Limit
- • Status: OPENED ✅
- ⏰ <b>Time:</b> {time_str}
- 📈 <b>Note:</b> New {position_type} position established
- 📊 Use /positions to view current holdings
- """
-
- await self.send_message(message.strip())
- logger.info(f"📢 Position opened: {token} {position_type} {amount} @ ${price}")
- async def _send_position_increased_notification(self, token: str, side: str, amount: float, price: float, position: Dict, action_type: str, time_str: str):
- """Send notification for position increase (additional entry)."""
- position_type = "LONG" if action_type == 'long_increased' else "SHORT"
- side_emoji = "🟢" if side.lower() == 'buy' else "🔴"
-
- total_size = abs(position['contracts'])
- avg_entry = position['avg_entry_price']
- entry_count = position['entry_count']
- total_value = total_size * avg_entry
-
- message = f"""
- 📈 <b>Position Increased</b>
- 📊 <b>{position_type} Position Updated:</b>
- • Token: {token}
- • Direction: {position_type}
- • Added Size: {amount} {token} @ ${price:,.2f}
- • New Total Size: {total_size} {token}
- • Average Entry: ${avg_entry:,.2f}
- {side_emoji} <b>Position Summary:</b>
- • Total Value: ${total_value:,.2f}
- • Entry Points: {entry_count}
- • Last Entry: ${price:,.2f}
- • Status: INCREASED ⬆️
- ⏰ <b>Time:</b> {time_str}
- 💡 <b>Strategy:</b> Multiple entry averaging
- 📊 Use /positions for complete position details
- """
-
- await self.send_message(message.strip())
- logger.info(f"📢 Position increased: {token} {position_type} +{amount} @ ${price} (total: {total_size})")
- async def _send_position_reduced_notification(self, token: str, side: str, amount: float, price: float, position: Dict, pnl_data: Dict, action_type: str, time_str: str):
- """Send notification for partial position close."""
- position_type = "LONG" if action_type == 'long_reduced' else "SHORT"
-
- remaining_size = abs(position['contracts'])
- avg_entry = pnl_data.get('avg_entry_price', position['avg_entry_price'])
- pnl = pnl_data['pnl']
- pnl_percent = pnl_data['pnl_percent']
- pnl_emoji = "🟢" if pnl >= 0 else "🔴"
-
- partial_value = amount * price
-
- message = f"""
- 📉 <b>Position Partially Closed</b>
- 📊 <b>{position_type} Partial Exit:</b>
- • Token: {token}
- • Direction: {position_type}
- • Closed Size: {amount} {token}
- • Exit Price: ${price:,.2f}
- • Remaining Size: {remaining_size} {token}
- {pnl_emoji} <b>Partial P&L:</b>
- • Entry Price: ${avg_entry:,.2f}
- • Exit Value: ${partial_value:,.2f}
- • P&L: ${pnl:,.2f} ({pnl_percent:+.2f}%)
- • Result: {"PROFIT" if pnl >= 0 else "LOSS"}
- 💰 <b>Position Status:</b>
- • Status: PARTIALLY CLOSED 📉
- • Take Profit Strategy: Active
- ⏰ <b>Time:</b> {time_str}
- 📊 Use /positions to view remaining position
- """
-
- await self.send_message(message.strip())
- logger.info(f"📢 Position reduced: {token} {position_type} -{amount} @ ${price} P&L: ${pnl:.2f}")
- async def _send_position_closed_notification(self, token: str, side: str, amount: float, price: float, position: Dict, pnl_data: Dict, action_type: str, time_str: str):
- """Send notification for fully closed position."""
- position_type = "LONG" if action_type == 'long_closed' else "SHORT"
-
- avg_entry = pnl_data.get('avg_entry_price', position['avg_entry_price'])
- pnl = pnl_data['pnl']
- pnl_percent = pnl_data['pnl_percent']
- pnl_emoji = "🟢" if pnl >= 0 else "🔴"
-
- entry_count = position.get('entry_count', 1)
- exit_value = amount * price
-
- message = f"""
- 🎯 <b>Position Fully Closed</b>
- 📊 <b>{position_type} Position Summary:</b>
- • Token: {token}
- • Direction: {position_type}
- • Total Size: {amount} {token}
- • Average Entry: ${avg_entry:,.2f}
- • Exit Price: ${price:,.2f}
- • Exit Value: ${exit_value:,.2f}
- {pnl_emoji} <b>Total P&L:</b>
- • P&L: ${pnl:,.2f} ({pnl_percent:+.2f}%)
- • Result: {"PROFIT" if pnl >= 0 else "LOSS"}
- • Entry Points Used: {entry_count}
- ✅ <b>Trade Complete:</b>
- • Status: FULLY CLOSED 🎯
- • Position: FLAT
- ⏰ <b>Time:</b> {time_str}
- 📊 Use /stats to view updated performance
- """
-
- await self.send_message(message.strip())
- logger.info(f"📢 Position closed: {token} {position_type} {amount} @ ${price} Total P&L: ${pnl:.2f}")
- async def _send_position_flipped_notification(self, token: str, side: str, amount: float, price: float, action_type: str, time_str: str):
- """Send notification for position flip (close and reverse)."""
- if action_type == 'long_closed_and_short_opened':
- old_type = "LONG"
- new_type = "SHORT"
- else:
- old_type = "SHORT"
- new_type = "LONG"
-
- message = f"""
- 🔄 <b>Position Flipped</b>
- 📊 <b>Direction Change:</b>
- • Token: {token}
- • Previous: {old_type} position
- • New: {new_type} position
- • Size: {amount} {token}
- • Price: ${price:,.2f}
- 🎯 <b>Trade Summary:</b>
- • {old_type} position: CLOSED ✅
- • {new_type} position: OPENED 🚀
- • Flip Price: ${price:,.2f}
- • Status: POSITION REVERSED
- ⏰ <b>Time:</b> {time_str}
- 💡 <b>Strategy:</b> Directional change
- 📊 Use /positions to view new position
- """
-
- await self.send_message(message.strip())
- logger.info(f"📢 Position flipped: {token} {old_type} -> {new_type} @ ${price}")
- 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()
-
- # Get balance adjustments info
- adjustments_summary = self.stats.get_balance_adjustments_summary()
- last_deposit_check = "Never"
- next_deposit_check = "Unknown"
-
- if hasattr(self, 'last_deposit_withdrawal_check') and self.last_deposit_withdrawal_check:
- last_deposit_check = self.last_deposit_withdrawal_check.strftime('%H:%M:%S')
- next_check_time = self.last_deposit_withdrawal_check + timedelta(seconds=self.deposit_withdrawal_check_interval)
- next_deposit_check = next_check_time.strftime('%H:%M:%S')
-
- # Safety checks for monitoring attributes
- monitoring_active = getattr(self, 'monitoring_active', False)
- last_known_orders = getattr(self, 'last_known_orders', set())
- last_known_positions = getattr(self, 'last_known_positions', {})
- deposit_withdrawal_check_interval = getattr(self, 'deposit_withdrawal_check_interval', 3600)
-
- status_text = f"""
- 🔄 <b>System Monitoring Status</b>
- 📊 <b>Order Monitoring:</b>
- • Active: {'✅ Yes' if monitoring_active else '❌ No'}
- • Check Interval: {Config.BOT_HEARTBEAT_SECONDS} seconds
- • Orders Tracked: {len(last_known_orders)}
- • Positions Tracked: {len(last_known_positions)}
- • Pending Stop Losses: {len(getattr(self, 'pending_stop_losses', {}))}
- 💰 <b>Deposit/Withdrawal Monitoring:</b>
- • Check Interval: {deposit_withdrawal_check_interval // 3600} hour(s)
- • Last Check: {last_deposit_check}
- • Next Check: {next_deposit_check}
- • Total Adjustments: {adjustments_summary['adjustment_count']}
- • Net Adjustment: ${adjustments_summary['net_adjustment']:,.2f}
- 🔔 <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'}
- • Order-based Stop Loss: ✅ Enabled
- 📈 <b>Notifications:</b>
- • 🚀 Position Opened/Increased
- • 📉 Position Partially/Fully Closed
- • 🎯 P&L Calculations
- • 🔔 Price Alarm Triggers
- • 🔄 External Trade Detection
- • 💰 Deposit/Withdrawal Detection
- • 🛑 Automatic Stop Loss Triggers
- • 🛑 Order-based Stop Loss Placement
- 💾 <b>Bot State Persistence:</b>
- • Pending Stop Losses: Saved to disk
- • Order Tracking: Saved to disk
- • External Trade Times: Saved to disk
- • Deposit Check Times: Saved to disk
- • State File: bot_state.json
- • Survives Bot Restarts: ✅ Yes
- ⏰ <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
- • Deposit/withdrawal tracking
- • Auto stats synchronization
- • Order-based stop loss placement
- • 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
- trading_days = 0
-
- for day_stats in daily_stats:
- if day_stats['has_trades']:
- # Day with completed trades
- pnl_emoji = "🟢" if day_stats['pnl'] >= 0 else "🔴"
- daily_text += f"📊 <b>{day_stats['date_formatted']}</b>\n"
- daily_text += f" {pnl_emoji} P&L: ${day_stats['pnl']:,.2f} ({day_stats['pnl_pct']:+.1f}%)\n"
- daily_text += f" 🔄 Trades: {day_stats['trades']}\n\n"
-
- total_pnl += day_stats['pnl']
- total_trades += day_stats['trades']
- trading_days += 1
- else:
- # Day with no trades
- daily_text += f"📊 <b>{day_stats['date_formatted']}</b>\n"
- daily_text += f" 📭 No completed trades\n\n"
-
- # Add summary
- if trading_days > 0:
- 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" 📈 Trading Days: {trading_days}/10\n"
- daily_text += f" 📊 Avg per Trading Day: ${total_pnl/trading_days:,.2f}"
- else:
- daily_text += f"💼 <b>10-Day Summary:</b>\n"
- daily_text += f" 📭 No completed trades in the last 10 days\n"
- daily_text += f" 💡 Start trading to see daily performance!"
-
- 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
- trading_weeks = 0
-
- for week_stats in weekly_stats:
- if week_stats['has_trades']:
- # Week with completed trades
- pnl_emoji = "🟢" if week_stats['pnl'] >= 0 else "🔴"
- weekly_text += f"📈 <b>{week_stats['week_formatted']}</b>\n"
- weekly_text += f" {pnl_emoji} P&L: ${week_stats['pnl']:,.2f} ({week_stats['pnl_pct']:+.1f}%)\n"
- weekly_text += f" 🔄 Trades: {week_stats['trades']}\n\n"
-
- total_pnl += week_stats['pnl']
- total_trades += week_stats['trades']
- trading_weeks += 1
- else:
- # Week with no trades
- weekly_text += f"📈 <b>{week_stats['week_formatted']}</b>\n"
- weekly_text += f" 📭 No completed trades\n\n"
-
- # Add summary
- if trading_weeks > 0:
- total_pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
- weekly_text += f"💼 <b>10-Week Summary:</b>\n"
- weekly_text += f" {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
- weekly_text += f" 🔄 Total Trades: {total_trades}\n"
- weekly_text += f" 📈 Trading Weeks: {trading_weeks}/10\n"
- weekly_text += f" 📊 Avg per Trading Week: ${total_pnl/trading_weeks:,.2f}"
- else:
- weekly_text += f"💼 <b>10-Week Summary:</b>\n"
- weekly_text += f" 📭 No completed trades in the last 10 weeks\n"
- weekly_text += f" 💡 Start trading to see weekly performance!"
-
- await 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
- trading_months = 0
-
- for month_stats in monthly_stats:
- if month_stats['has_trades']:
- # Month with completed trades
- pnl_emoji = "🟢" if month_stats['pnl'] >= 0 else "🔴"
- monthly_text += f"📅 <b>{month_stats['month_formatted']}</b>\n"
- monthly_text += f" {pnl_emoji} P&L: ${month_stats['pnl']:,.2f} ({month_stats['pnl_pct']:+.1f}%)\n"
- monthly_text += f" 🔄 Trades: {month_stats['trades']}\n\n"
-
- total_pnl += month_stats['pnl']
- total_trades += month_stats['trades']
- trading_months += 1
- else:
- # Month with no trades
- monthly_text += f"📅 <b>{month_stats['month_formatted']}</b>\n"
- monthly_text += f" 📭 No completed trades\n\n"
-
- # Add summary
- if trading_months > 0:
- total_pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
- monthly_text += f"💼 <b>10-Month Summary:</b>\n"
- monthly_text += f" {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
- monthly_text += f" 🔄 Total Trades: {total_trades}\n"
- monthly_text += f" 📈 Trading Months: {trading_months}/10\n"
- monthly_text += f" 📊 Avg per Trading Month: ${total_pnl/trading_months:,.2f}"
- else:
- monthly_text += f"💼 <b>10-Month Summary:</b>\n"
- monthly_text += f" 📭 No completed trades in the last 10 months\n"
- monthly_text += f" 💡 Start trading to see monthly performance!"
-
- await 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 risk_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /risk command to show advanced risk metrics."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("❌ Unauthorized access.")
- return
-
- try:
- # Get current balance for context
- balance = self.client.get_balance()
- current_balance = 0
- if balance and balance.get('total'):
- current_balance = float(balance['total'].get('USDC', 0))
-
- # Get risk metrics and basic stats
- risk_metrics = self.stats.get_risk_metrics()
- basic_stats = self.stats.get_basic_stats()
-
- # Check if we have enough data for risk calculations
- if basic_stats['completed_trades'] < 2:
- await update.message.reply_text(
- "📊 <b>Risk Analysis</b>\n\n"
- "📭 <b>Insufficient Data</b>\n\n"
- f"• Current completed trades: {basic_stats['completed_trades']}\n"
- f"• Required for risk analysis: 2+ trades\n"
- f"• Daily balance snapshots: {len(self.stats.data.get('daily_balances', []))}\n\n"
- "💡 <b>To enable risk analysis:</b>\n"
- "• Complete more trades to generate returns data\n"
- "• Bot automatically records daily balance snapshots\n"
- "• Risk metrics will be available after sufficient trading history\n\n"
- "📈 Use /stats for current performance metrics",
- parse_mode='HTML'
- )
- return
-
- # Format the risk analysis message
- risk_text = f"""
- 📊 <b>Risk Analysis & Advanced Metrics</b>
- 🎯 <b>Risk-Adjusted Performance:</b>
- • Sharpe Ratio: {risk_metrics['sharpe_ratio']:.3f}
- • Sortino Ratio: {risk_metrics['sortino_ratio']:.3f}
- • Annual Volatility: {risk_metrics['volatility']:.2f}%
- 📉 <b>Drawdown Analysis:</b>
- • Maximum Drawdown: {risk_metrics['max_drawdown']:.2f}%
- • Value at Risk (95%): {risk_metrics['var_95']:.2f}%
- 💰 <b>Portfolio Context:</b>
- • Current Balance: ${current_balance:,.2f}
- • Initial Balance: ${basic_stats['initial_balance']:,.2f}
- • Total P&L: ${basic_stats['total_pnl']:,.2f}
- • Days Active: {basic_stats['days_active']}
- 📊 <b>Risk Interpretation:</b>
- """
-
- # Add interpretive guidance
- sharpe = risk_metrics['sharpe_ratio']
- if sharpe > 2.0:
- risk_text += "• 🟢 <b>Excellent</b> risk-adjusted returns (Sharpe > 2.0)\n"
- elif sharpe > 1.0:
- risk_text += "• 🟡 <b>Good</b> risk-adjusted returns (Sharpe > 1.0)\n"
- elif sharpe > 0.5:
- risk_text += "• 🟠 <b>Moderate</b> risk-adjusted returns (Sharpe > 0.5)\n"
- elif sharpe > 0:
- risk_text += "• 🔴 <b>Poor</b> risk-adjusted returns (Sharpe > 0)\n"
- else:
- risk_text += "• ⚫ <b>Negative</b> risk-adjusted returns (Sharpe < 0)\n"
-
- max_dd = risk_metrics['max_drawdown']
- if max_dd < 5:
- risk_text += "• 🟢 <b>Low</b> maximum drawdown (< 5%)\n"
- elif max_dd < 15:
- risk_text += "• 🟡 <b>Moderate</b> maximum drawdown (< 15%)\n"
- elif max_dd < 30:
- risk_text += "• 🟠 <b>High</b> maximum drawdown (< 30%)\n"
- else:
- risk_text += "• 🔴 <b>Very High</b> maximum drawdown (> 30%)\n"
-
- volatility = risk_metrics['volatility']
- if volatility < 10:
- risk_text += "• 🟢 <b>Low</b> portfolio volatility (< 10%)\n"
- elif volatility < 25:
- risk_text += "• 🟡 <b>Moderate</b> portfolio volatility (< 25%)\n"
- elif volatility < 50:
- risk_text += "• 🟠 <b>High</b> portfolio volatility (< 50%)\n"
- else:
- risk_text += "• 🔴 <b>Very High</b> portfolio volatility (> 50%)\n"
-
- risk_text += f"""
- 💡 <b>Risk Definitions:</b>
- • <b>Sharpe Ratio:</b> Risk-adjusted return (excess return / volatility)
- • <b>Sortino Ratio:</b> Return / downside volatility (focuses on bad volatility)
- • <b>Max Drawdown:</b> Largest peak-to-trough decline
- • <b>VaR 95%:</b> Maximum expected loss 95% of the time
- • <b>Volatility:</b> Annualized standard deviation of returns
- 📈 <b>Data Based On:</b>
- • Completed Trades: {basic_stats['completed_trades']}
- • Daily Balance Records: {len(self.stats.data.get('daily_balances', []))}
- • Trading Period: {basic_stats['days_active']} days
- 🔄 Use /stats for trading performance metrics
- """
-
- await update.message.reply_text(risk_text.strip(), parse_mode='HTML')
-
- except Exception as e:
- error_message = f"❌ Error processing risk command: {str(e)}"
- await update.message.reply_text(error_message)
- logger.error(f"Error in risk command: {e}")
-
- async def version_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /version command to show bot version and system info."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("❌ Unauthorized access.")
- return
-
- try:
- # Get system info
- import platform
- import sys
- from datetime import datetime
-
- uptime_info = "Unknown"
- try:
- # Try to get process uptime if available
- import psutil
- process = psutil.Process()
- create_time = datetime.fromtimestamp(process.create_time())
- uptime = datetime.now() - create_time
- days = uptime.days
- hours, remainder = divmod(uptime.seconds, 3600)
- minutes, _ = divmod(remainder, 60)
- uptime_info = f"{days}d {hours}h {minutes}m"
- except ImportError:
- # psutil not available, skip uptime
- pass
-
- # Get stats info
- basic_stats = self.stats.get_basic_stats()
-
- # Safety checks for monitoring attributes
- order_monitoring_task = getattr(self, 'order_monitoring_task', None)
- alarms = getattr(self, 'alarms', [])
-
- version_text = f"""
- 🤖 <b>Trading Bot Version & System Info</b>
- 📱 <b>Bot Information:</b>
- • Version: <code>{self.version}</code>
- • Network: {'Testnet' if Config.HYPERLIQUID_TESTNET else 'Mainnet'}
- • Uptime: {uptime_info}
- • Default Token: {Config.DEFAULT_TRADING_TOKEN}
- 💻 <b>System Information:</b>
- • Python: {sys.version.split()[0]}
- • Platform: {platform.system()} {platform.release()}
- • Architecture: {platform.machine()}
- 📊 <b>Trading Stats:</b>
- • Total Orders: {basic_stats['total_trades']}
- • Completed Trades: {basic_stats['completed_trades']}
- • Days Active: {basic_stats['days_active']}
- • Start Date: {basic_stats['start_date']}
- 🔄 <b>Monitoring Status:</b>
- • Order Monitoring: {'✅ Active' if order_monitoring_task and not order_monitoring_task.done() else '❌ Inactive'}
- • External Trades: ✅ Active
- • Price Alarms: ✅ Active ({len(alarms)} active)
- • Risk Management: {'✅ Enabled' if Config.RISK_MANAGEMENT_ENABLED else '❌ Disabled'}
- ⏰ <b>Current Time:</b> {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
- """
-
- await update.message.reply_text(version_text.strip(), parse_mode='HTML')
-
- except Exception as e:
- error_message = f"❌ Error processing version command: {str(e)}"
- await update.message.reply_text(error_message)
- logger.error(f"Error in version command: {e}")
-
- async def balance_adjustments_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /balance_adjustments command to show deposit/withdrawal history."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("❌ Unauthorized access.")
- return
-
- try:
- # Get balance adjustments summary
- adjustments_summary = self.stats.get_balance_adjustments_summary()
-
- # Get detailed adjustments
- all_adjustments = self.stats.data.get('balance_adjustments', [])
-
- if not all_adjustments:
- await update.message.reply_text(
- "💰 <b>Balance Adjustments</b>\n\n"
- "📭 No deposits or withdrawals detected yet.\n\n"
- "💡 The bot automatically monitors for deposits and withdrawals\n"
- "every hour to maintain accurate P&L calculations.",
- parse_mode='HTML'
- )
- return
-
- # Format the message
- adjustments_text = f"""
- 💰 <b>Balance Adjustments History</b>
- 📊 <b>Summary:</b>
- • Total Deposits: ${adjustments_summary['total_deposits']:,.2f}
- • Total Withdrawals: ${adjustments_summary['total_withdrawals']:,.2f}
- • Net Adjustment: ${adjustments_summary['net_adjustment']:,.2f}
- • Total Transactions: {adjustments_summary['adjustment_count']}
- 📅 <b>Recent Adjustments:</b>
- """
-
- # Show last 10 adjustments
- recent_adjustments = sorted(all_adjustments, key=lambda x: x['timestamp'], reverse=True)[:10]
-
- for adj in recent_adjustments:
- try:
- # Format timestamp
- adj_time = datetime.fromisoformat(adj['timestamp']).strftime('%m/%d %H:%M')
-
- # Format type and amount
- if adj['type'] == 'deposit':
- emoji = "💰"
- amount_str = f"+${adj['amount']:,.2f}"
- else: # withdrawal
- emoji = "💸"
- amount_str = f"-${abs(adj['amount']):,.2f}"
-
- adjustments_text += f"• {emoji} {adj_time}: {amount_str}\n"
-
- except Exception as adj_error:
- logger.warning(f"Error formatting adjustment: {adj_error}")
- continue
-
- adjustments_text += f"""
- 💡 <b>How it Works:</b>
- • Bot checks for deposits/withdrawals every hour
- • Adjustments maintain accurate P&L calculations
- • Non-trading balance changes don't affect performance metrics
- • Trading statistics remain pure and accurate
- ⏰ <b>Last Check:</b> {adjustments_summary['last_adjustment'][:16] if adjustments_summary['last_adjustment'] else 'Never'}
- """
-
- await update.message.reply_text(adjustments_text.strip(), parse_mode='HTML')
-
- except Exception as e:
- error_message = f"❌ Error processing balance adjustments command: {str(e)}"
- await update.message.reply_text(error_message)
- logger.error(f"Error in balance_adjustments command: {e}")
- def _get_position_state(self, symbol: str) -> Dict[str, Any]:
- """Get current position state for a symbol."""
- if symbol not in self.position_tracker:
- self.position_tracker[symbol] = {
- 'contracts': 0.0,
- 'avg_entry_price': 0.0,
- 'total_cost_basis': 0.0,
- 'entry_count': 0,
- 'entry_history': [], # List of {price, amount, timestamp}
- 'last_update': datetime.now().isoformat()
- }
- return self.position_tracker[symbol]
-
- def _update_position_state(self, symbol: str, side: str, amount: float, price: float, timestamp: str = None):
- """Update position state with a new trade."""
- if timestamp is None:
- timestamp = datetime.now().isoformat()
-
- position = self._get_position_state(symbol)
-
- if side.lower() == 'buy':
- # Adding to long position or reducing short position
- if position['contracts'] >= 0:
- # Opening/adding to long position
- new_cost = amount * price
- old_cost = position['total_cost_basis']
- old_contracts = position['contracts']
-
- position['contracts'] += amount
- position['total_cost_basis'] += new_cost
- position['avg_entry_price'] = position['total_cost_basis'] / position['contracts'] if position['contracts'] > 0 else 0
- position['entry_count'] += 1
- position['entry_history'].append({
- 'price': price,
- 'amount': amount,
- 'timestamp': timestamp,
- 'side': 'buy'
- })
-
- logger.info(f"📈 Position updated: {symbol} LONG {position['contracts']:.6f} @ avg ${position['avg_entry_price']:.2f}")
- return 'long_opened' if old_contracts == 0 else 'long_increased'
- else:
- # Reducing short position
- reduction = min(amount, abs(position['contracts']))
- position['contracts'] += reduction
-
- if position['contracts'] >= 0:
- # Short position fully closed or flipped to long
- if position['contracts'] == 0:
- self._reset_position_state(symbol)
- return 'short_closed'
- else:
- # Flipped to long - need to track new long position
- remaining_amount = amount - reduction
- position['contracts'] = remaining_amount
- position['total_cost_basis'] = remaining_amount * price
- position['avg_entry_price'] = price
- return 'short_closed_and_long_opened'
- else:
- return 'short_reduced'
-
- elif side.lower() == 'sell':
- # Adding to short position or reducing long position
- if position['contracts'] <= 0:
- # Opening/adding to short position
- position['contracts'] -= amount
- position['entry_count'] += 1
- position['entry_history'].append({
- 'price': price,
- 'amount': amount,
- 'timestamp': timestamp,
- 'side': 'sell'
- })
-
- logger.info(f"📉 Position updated: {symbol} SHORT {abs(position['contracts']):.6f} @ ${price:.2f}")
- return 'short_opened' if position['contracts'] == -amount else 'short_increased'
- else:
- # Reducing long position
- reduction = min(amount, position['contracts'])
- position['contracts'] -= reduction
-
- # Adjust cost basis proportionally
- if position['contracts'] > 0:
- reduction_ratio = reduction / (position['contracts'] + reduction)
- position['total_cost_basis'] *= (1 - reduction_ratio)
- return 'long_reduced'
- else:
- # Long position fully closed
- if position['contracts'] == 0:
- self._reset_position_state(symbol)
- return 'long_closed'
- else:
- # Flipped to short
- remaining_amount = amount - reduction
- position['contracts'] = -remaining_amount
- return 'long_closed_and_short_opened'
-
- position['last_update'] = timestamp
- return 'unknown'
-
- def _reset_position_state(self, symbol: str):
- """Reset position state when position is fully closed."""
- if symbol in self.position_tracker:
- del self.position_tracker[symbol]
-
- def _calculate_position_pnl(self, symbol: str, exit_amount: float, exit_price: float) -> Dict[str, float]:
- """Calculate P&L for a position exit."""
- position = self._get_position_state(symbol)
-
- if position['contracts'] == 0:
- return {'pnl': 0.0, 'pnl_percent': 0.0}
-
- avg_entry = position['avg_entry_price']
-
- if position['contracts'] > 0: # Long position
- pnl = exit_amount * (exit_price - avg_entry)
- else: # Short position
- pnl = exit_amount * (avg_entry - exit_price)
-
- cost_basis = exit_amount * avg_entry
- pnl_percent = (pnl / cost_basis * 100) if cost_basis > 0 else 0
-
- return {
- 'pnl': pnl,
- 'pnl_percent': pnl_percent,
- 'avg_entry_price': avg_entry
- }
- async def _send_external_trade_notification(self, trade: Dict[str, Any]):
- """Send generic notification for external trades (fallback)."""
- 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> External 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 generic 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 and update position tracking
- action_type = self.stats.record_trade_with_enhanced_tracking(symbol, exit_side, contracts_abs, current_price, order.get('id', 'stop_loss'), "auto_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 using enhanced position tracking."""
- try:
- # For bot-initiated orders, we'll detect changes in position size
- # and send appropriate notifications using the enhanced system
-
- # This method will be triggered when orders placed through the bot are filled
- # The external trade monitoring will handle trades made outside the bot
-
- # Update position tracking based on current positions
- await self._update_position_tracking(current_positions)
-
- except Exception as e:
- logger.error(f"❌ Error processing filled orders: {e}")
-
- async def _update_position_tracking(self, current_positions: list):
- """Update the legacy position tracking data for compatibility."""
- 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
- }
-
- # Also update our enhanced position tracker if not already present
- if symbol not in self.position_tracker:
- self._get_position_state(symbol)
- self.position_tracker[symbol]['contracts'] = contracts
- self.position_tracker[symbol]['avg_entry_price'] = entry_price
- self.position_tracker[symbol]['total_cost_basis'] = contracts * entry_price
-
- self.last_known_positions = new_position_map
- async def keyboard_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /keyboard command to enable/show custom keyboard."""
- if not self.is_authorized(update.effective_chat.id):
- await update.message.reply_text("❌ Unauthorized access.")
- return
-
- custom_keyboard = self._create_custom_keyboard()
-
- if custom_keyboard:
- await update.message.reply_text(
- "⌨️ <b>Custom Keyboard Activated!</b>\n\n"
- "🎯 <b>Your quick buttons are now ready:</b>\n"
- "• Daily - Daily performance\n"
- "• Performance - Performance stats\n"
- "• Balance - Account balance\n"
- "• Stats - Trading statistics\n"
- "• Positions - Open positions\n"
- "• Orders - Active orders\n"
- "• Price - Quick price check\n"
- "• Market - Market overview\n"
- "• Help - Help guide\n"
- "• Commands - Command menu\n\n"
- "💡 <b>How to use:</b>\n"
- "Tap any button below instead of typing the command manually!\n\n"
- "🔧 These buttons will stay at the bottom of your chat.",
- parse_mode='HTML',
- reply_markup=custom_keyboard
- )
- else:
- await update.message.reply_text(
- "❌ <b>Custom Keyboard Disabled</b>\n\n"
- "🔧 <b>To enable:</b>\n"
- "• Set TELEGRAM_CUSTOM_KEYBOARD_ENABLED=true in your .env file\n"
- "• Restart the bot\n"
- "• Run /keyboard again\n\n"
- f"📋 <b>Current config:</b>\n"
- f"• Enabled: {Config.TELEGRAM_CUSTOM_KEYBOARD_ENABLED}\n"
- f"• Layout: {Config.TELEGRAM_CUSTOM_KEYBOARD_LAYOUT}",
- parse_mode='HTML'
- )
- async def handle_keyboard_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle messages from custom keyboard buttons (without /)."""
- if not self.is_authorized(update.effective_chat.id):
- return
-
- message_text = update.message.text.lower()
-
- # Map clean button text to command handlers
- command_handlers = {
- 'daily': self.daily_command,
- 'performance': self.performance_command,
- 'balance': self.balance_command,
- 'stats': self.stats_command,
- 'positions': self.positions_command,
- 'orders': self.orders_command,
- 'price': self.price_command,
- 'market': self.market_command,
- 'help': self.help_command,
- 'commands': self.commands_command
- }
-
- # Execute the corresponding command handler
- if message_text in command_handlers:
- await command_handlers[message_text](update, context)
- 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()
|