diff --git a/src/strategies/ping_pong_bot.py b/src/strategies/ping_pong_bot.py index 04eafb4..ba499d7 100644 --- a/src/strategies/ping_pong_bot.py +++ b/src/strategies/ping_pong_bot.py @@ -15,7 +15,6 @@ from rich.console import Console from rich.table import Table from rich.panel import Panel from rich.layout import Layout -from rich.live import Live from rich import box import asyncpg @@ -69,7 +68,7 @@ class DatabaseManager: class PingPongBot: def __init__(self, config_path="config/ping_pong_config.yaml"): - self.version = "1.3.5" + self.version = "1.3.6" with open(config_path, 'r') as f: self.config = yaml.safe_load(f) @@ -79,16 +78,18 @@ class PingPongBot: if not self.api_key or not self.api_secret: raise ValueError("API_KEY and API_SECRET must be set in .env file") + # Added timeout to prevent hanging self.session = HTTP( testnet=False, api_key=self.api_key, api_secret=self.api_secret, + request_timeout=10 ) self.db = DatabaseManager() - self.symbol = self.config['symbol'].upper() # e.g. BTCUSDT - self.db_symbol = self.symbol.replace("USDT", "") # e.g. BTC + self.symbol = self.config['symbol'].upper() + self.db_symbol = self.symbol.replace("USDT", "") self.interval = str(self.config['interval']) self.db_interval = self.interval + "m" if self.interval.isdigit() else self.interval @@ -108,7 +109,7 @@ class PingPongBot: self.status_msg = "Initializing..." self.last_signal = None self.start_time = datetime.now() - self.last_refresh_time = datetime.now() + self.last_heartbeat = datetime.now() self.console = Console() # Parameters @@ -124,14 +125,12 @@ class PingPongBot: return series.ewm(alpha=alpha, adjust=False).mean() def calculate_indicators(self, df): - # RSI rsi_cfg = self.config['rsi'] delta = df['close'].diff() gain = delta.where(delta > 0, 0) loss = -delta.where(delta < 0, 0) df['rsi'] = 100 - (100 / (1 + (self.rma(gain, rsi_cfg['period']) / self.rma(loss, rsi_cfg['period'])))) - # Hurst hurst_cfg = self.config['hurst'] mcl = hurst_cfg['period'] / 2 mcl_2 = int(round(mcl / 2)) @@ -160,7 +159,6 @@ class PingPongBot: return df async def update_exchange_data(self): - """Fetch Price, Balance, Position every 15s""" try: ticker = self.session.get_tickers(category="linear", symbol=self.symbol) if ticker['retCode'] == 0: @@ -236,86 +234,88 @@ class PingPongBot: except Exception as e: logger.error(f"Trade Error: {e}") - def generate_dashboard(self): - """Generates the dashboard layout""" - layout = Layout() - layout.split_column( - Layout(name="header", size=3), - Layout(name="indicators", size=7), - Layout(name="position", size=6), - Layout(name="footer", size=3) - ) + def print_dashboard(self): + """Prints a clean summary to the logs (replaces Live which can hang in Docker)""" + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + self.last_heartbeat = datetime.now() - # Header - header_table = Table(box=box.ROUNDED, expand=True, show_header=False) - header_table.add_row(f"[bold cyan]PING-PONG BOT v{self.version}[/] | Symbol: [bold white]{self.symbol}[/] | Price: [bold yellow]${self.market_price:,.2f}[/]") - layout["header"].update(Panel(header_table)) + # 1. Header + header = Table(title=f"PING-PONG BOT v{self.version} Dashboard", box=box.ROUNDED, expand=True) + header.add_column("Property"); header.add_column("Value") + header.add_row("Symbol", self.symbol) + header.add_row("Market Price", f"${self.market_price:,.2f}") + header.add_row("Direction", self.direction.upper()) + header.add_row("Last Candle", f"{self.last_candle_time} (@${self.last_candle_price:,.2f})") + + # 2. Indicators + inds = Table(title="INDICATORS", box=box.ROUNDED, expand=True) + inds.add_column("Indicator"); inds.add_column("Value"); inds.add_column("Updated") + inds.add_row("Hurst Upper", f"{self.current_indicators['hurst_upper']['value']:.2f}", self.current_indicators['hurst_upper']['timestamp']) + inds.add_row("Hurst Lower", f"{self.current_indicators['hurst_lower']['value']:.2f}", self.current_indicators['hurst_lower']['timestamp']) + inds.add_row("RSI", f"{self.current_indicators['rsi']['value']:.2f}", self.current_indicators['rsi']['timestamp']) - # Indicators - ind_table = Table(box=box.ROUNDED, expand=True) - ind_table.add_column("Indicator", style="dim"); ind_table.add_column("Value", justify="right"); ind_table.add_column("Last Update", justify="center") - ind_table.add_row("Hurst Upper", f"{self.current_indicators['hurst_upper']['value']:.2f}", self.current_indicators['hurst_upper']['timestamp']) - ind_table.add_row("Hurst Lower", f"{self.current_indicators['hurst_lower']['value']:.2f}", self.current_indicators['hurst_lower']['timestamp']) - ind_table.add_row("RSI", f"{self.current_indicators['rsi']['value']:.2f}", self.current_indicators['rsi']['timestamp']) - layout["indicators"].update(Panel(ind_table, title="[bold yellow]TECHNICAL INDICATORS[/]")) - - # Position - pos_table = Table(box=box.ROUNDED, expand=True) - pos_table.add_column("Account Balance", justify="center"); pos_table.add_column("Position Size", justify="center"); pos_table.add_column("Entry Price", justify="center"); pos_table.add_column("Unrealized PnL", justify="center") + # 3. Position + pos = Table(title="PORTFOLIO & STATUS", box=box.ROUNDED, expand=True) + pos.add_column("Property"); pos.add_column("Value") + pos.add_row("Wallet Balance", f"${self.wallet_balance:,.2f}") if self.position: pnl = float(self.position['unrealisedPnl']) - pos_table.add_row(f"${self.wallet_balance:,.2f}", str(self.position['size']), f"${float(self.position['avgPrice']):,.2f}", f"[bold {'green' if pnl>=0 else 'red'}]${pnl:,.2f}") + pos.add_row("Position Size", str(self.position['size'])) + pos.add_row("Entry Price", f"${float(self.position['avgPrice']):,.2f}") + pos.add_row("Unrealized PnL", f"[bold {'green' if pnl>=0 else 'red'}]${pnl:,.2f}") else: - pos_table.add_row(f"${self.wallet_balance:,.2f}", "0", "-", "-") - layout["position"].update(Panel(pos_table, title="[bold green]PORTFOLIO STATUS[/]")) - - # Footer - refresh_str = self.last_refresh_time.strftime("%H:%M:%S") - footer_text = f"Status: [bold blue]{self.status_msg}[/]\nLast Signal: [bold yellow]{self.last_signal or 'N/A'}[/] | Last Candle: {self.last_candle_time or 'N/A'} | [dim italic]Heartbeat: {refresh_str}[/]" - layout["footer"].update(Panel(footer_text)) + pos.add_row("Position", "NONE") - return layout + pos.add_row("Status", f"[bold blue]{self.status_msg}[/]") + pos.add_row("Last Signal", str(self.last_signal or "None")) + pos.add_row("Heartbeat", f"[italic]{now}[/]") + + self.console.print("\n") + self.console.print(header) + self.console.print(inds) + self.console.print(pos) + self.console.print("-" * 50) async def run(self): + logger.info("Bot starting...") await self.db.connect() last_exchange_update = 0 - with Live(self.generate_dashboard(), refresh_per_second=1) as live: - while True: - try: - now = time.time() - self.last_refresh_time = datetime.now() - - # 1. Exchange Sync (15s) - if now - last_exchange_update >= 15: - await self.update_exchange_data() - last_exchange_update = now - - # 2. DB Sync (5s) - candles = await self.db.get_candles(self.db_symbol, self.db_interval, limit=100) - if candles: - latest = candles[0] - if latest['time'] != self.last_candle_time: - df = pd.DataFrame(candles[::-1]) - df = df.astype({'open': float, 'high': float, 'low': float, 'close': float, 'volume': float}) - df = self.calculate_indicators(df) - signal = self.check_signals(df) - if signal: await self.execute_trade(signal) - self.last_candle_time = latest['time'] - self.last_candle_price = latest['close'] - self.status_msg = f"New Candle processed: {latest['time']}" - else: - self.status_msg = f"No candles found for {self.db_symbol} / {self.db_interval}" - - # 3. Update Dashboard - live.update(self.generate_dashboard()) - - except Exception as e: - logger.exception(f"Loop Error: {e}") - self.status_msg = f"Error: {str(e)[:50]}" - live.update(self.generate_dashboard()) + while True: + try: + now_ts = time.time() - await asyncio.sleep(5) + # 1. Exchange Sync (15s) + if now_ts - last_exchange_update >= 15: + await self.update_exchange_data() + last_exchange_update = now_ts + + # 2. DB Sync (5s) + candles = await self.db.get_candles(self.db_symbol, self.db_interval, limit=100) + if candles: + latest = candles[0] + if latest['time'] != self.last_candle_time: + df = pd.DataFrame(candles[::-1]) + df = df.astype({'open': float, 'high': float, 'low': float, 'close': float, 'volume': float}) + df = self.calculate_indicators(df) + signal = self.check_signals(df) + if signal: + logger.info(f"Signal detected: {signal}") + await self.execute_trade(signal) + self.last_candle_time = latest['time'] + self.last_candle_price = latest['close'] + self.status_msg = f"New Candle: {latest['time']}" + else: + self.status_msg = f"No candles found for {self.db_symbol}/{self.db_interval}" + + # 3. Print Dashboard + self.print_dashboard() + + except Exception as e: + logger.exception(f"Loop Error: {e}") + self.status_msg = f"Error: {str(e)[:50]}" + + await asyncio.sleep(5) if __name__ == "__main__": bot = PingPongBot()