diff --git a/src/strategies/ping_pong_bot.py b/src/strategies/ping_pong_bot.py index a91f07d..dff2942 100644 --- a/src/strategies/ping_pong_bot.py +++ b/src/strategies/ping_pong_bot.py @@ -86,11 +86,10 @@ class DatabaseManager: class PingPongBot: def __init__(self, config_path="config/ping_pong_config.yaml"): - self.version = "1.5.4" + self.version = "1.5.5" with open(config_path, 'r') as f: self.config = yaml.safe_load(f) - # Explicitly load from ENV to ensure they are available self.api_key = os.getenv("BYBIT_API_KEY") or os.getenv("API_KEY") self.api_secret = os.getenv("BYBIT_API_SECRET") or os.getenv("API_SECRET") @@ -104,7 +103,6 @@ class PingPongBot: timeout=10 ) - # Initialize DB with explicit credentials self.db = DatabaseManager( host=os.getenv('DB_HOST', '20.20.20.20'), port=os.getenv('DB_PORT', 5433), @@ -113,22 +111,17 @@ class PingPongBot: password=os.getenv('DB_PASSWORD', '') ) - # Base settings - Improved extraction raw_symbol = self.config['symbol'].upper() - # Remove common suffixes to get base coin (e.g., BTCUSD -> BTC, BTCUSDT -> BTC) self.base_coin = raw_symbol.replace("USDT", "").replace("USDC", "").replace("USD", "") - self.db_symbol = self.base_coin self.interval = str(self.config['interval']) - # Map interval to DB format: '30' -> '30m' self.db_interval = self.interval + "m" if self.interval.isdigit() else self.interval - logger.info(f"Bot v{self.version} Initialized | DB Symbol: {self.db_symbol} | DB Interval: {self.db_interval}") - # Dynamic Strategy State - self.direction = None # 'long' or 'short' - self.category = None # 'linear' or 'inverse' - self.symbol = None # 'BTCUSDC' or 'BTCUSD' + self.direction = None + self.category = None + self.symbol = None + self.settle_coin = None # Tracking for SMA(44, 1D) self.ma_44_val = 0.0 @@ -150,7 +143,6 @@ class PingPongBot: self.start_time = datetime.now() self.console = Console() - # Fixed Parameters from Config self.partial_exit_pct = float(self.config.get('partial_exit_pct', 0.15)) self.min_val_usd = float(self.config.get('min_position_value_usd', 15.0)) self.pos_size_margin = float(self.config.get('pos_size_margin', 20.0)) @@ -162,14 +154,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)) @@ -189,14 +179,12 @@ class PingPongBot: return df async def update_direction(self): - """Logic Point I: 1D MA44 check and Point II: Asset/Perp selection""" try: logger.info(f"Checking direction based on SMA(44, 1D) for {self.db_symbol}...") candles_1d = await self.db.get_candles(self.db_symbol, "1d", limit=100) if not candles_1d or len(candles_1d) < 44: got = len(candles_1d) if candles_1d else 0 - logger.warning(f"Not enough 1D data for MA44. Got {got} candles for {self.db_symbol} / 1d.") self.status_msg = f"Error: Need 44 1D candles (Got {got})" return False @@ -204,11 +192,8 @@ class PingPongBot: df_1d['close'] = df_1d['close'].astype(float) self.ma_44_val = df_1d['close'].rolling(window=44).mean().iloc[-1] - # Use BTCUSDT for price check if not initialized, otherwise use current symbol - ticker_symbol = self.symbol if self.symbol else f"{self.base_coin}USDT" - ticker_cat = self.category if self.category else "linear" - - ticker = await asyncio.to_thread(self.session.get_tickers, category=ticker_cat, symbol=ticker_symbol) + # Use BTCUSDT (Linear) for reliable initial price check + ticker = await asyncio.to_thread(self.session.get_tickers, category="linear", symbol=f"{self.base_coin}USDT") current_price = float(ticker['result']['list'][0]['lastPrice']) self.market_price = current_price @@ -227,9 +212,12 @@ class PingPongBot: if self.direction == "long": self.category = "inverse" self.symbol = f"{self.base_coin}USD" + self.settle_coin = self.base_coin else: self.category = "linear" - self.symbol = f"{self.base_coin}USDC" + # BTCPERP is the Bybit symbol for USDC Linear Perpetual (BTCUSDC display name) + self.symbol = "BTCPERP" if self.base_coin == "BTC" else f"{self.base_coin}USDC" + self.settle_coin = "USDC" logger.info(f"Bot configured for {self.direction.upper()} | Symbol: {self.symbol} | Category: {self.category}") self.last_candle_time = None @@ -242,7 +230,6 @@ class PingPongBot: return False async def close_all_positions(self): - """Closes any active position in the current category/symbol""" try: if not self.category or not self.symbol: return pos = await asyncio.to_thread(self.session.get_positions, category=self.category, symbol=self.symbol) @@ -255,17 +242,14 @@ class PingPongBot: logger.error(f"Error closing positions: {e}") async def swap_assets(self, target_direction): - """Point II: Exchange BTC/USDC on Spot market""" try: logger.info(f"Swapping assets for {target_direction.upper()} mode...") spot_symbol = f"{self.base_coin}USDC" - # Get Spot Balances balance = await asyncio.to_thread(self.session.get_wallet_balance, category="spot", coin=f"{self.base_coin},USDC") coins = {c['coin']: float(c['walletBalance']) for c in balance['result']['list'][0]['coin']} if target_direction == "short": - # Need USDC: Sell all BTC btc_bal = coins.get(self.base_coin, 0) if btc_bal > 0.0001: logger.info(f"Spot: Selling {btc_bal} {self.base_coin} for USDC") @@ -273,7 +257,6 @@ class PingPongBot: category="spot", symbol=spot_symbol, side="Sell", orderType="Market", qty=str(btc_bal) ) else: - # Need BTC: Buy BTC with all USDC usdc_bal = coins.get("USDC", 0) if usdc_bal > 1.0: logger.info(f"Spot: Buying {self.base_coin} with {usdc_bal} USDC") @@ -282,27 +265,23 @@ class PingPongBot: qty=str(usdc_bal), marketUnit="quote" ) - await asyncio.sleep(2) # Wait for spot settlement + await asyncio.sleep(2) except Exception as e: logger.error(f"Asset Swap Error: {e}") async def update_exchange_data(self): - """Fetch Price, Balance, Position every 15s""" if not self.category or not self.symbol: return try: ticker = await asyncio.to_thread(self.session.get_tickers, category=self.category, symbol=self.symbol) if ticker['retCode'] == 0: self.market_price = float(ticker['result']['list'][0]['lastPrice']) - # settleCoin is only for USDC linear perpetuals - settle_coin = "USDC" if (self.category == "linear" and "USDC" in self.symbol) else None - pos = await asyncio.to_thread(self.session.get_positions, category=self.category, symbol=self.symbol, settleCoin=settle_coin) + pos = await asyncio.to_thread(self.session.get_positions, category=self.category, symbol=self.symbol, settleCoin=self.settle_coin if self.category == "linear" else None) if pos['retCode'] == 0: active = [p for p in pos['result']['list'] if float(p.get('size', 0)) > 0] self.position = active[0] if active else None - # Use appropriate coin for balance based on category - target_coin = "USDC" if self.category == "linear" else self.base_coin + target_coin = self.settle_coin wallet = await asyncio.to_thread(self.session.get_wallet_balance, category=self.category, accountType="UNIFIED", coin=target_coin) if wallet['retCode'] == 0: res_list = wallet['result']['list'] @@ -416,17 +395,14 @@ class PingPongBot: try: now = time.time() - # 1. Periodically check direction (every 2m) if now - self.last_ma_check_time >= 120: await self.update_direction() self.last_ma_check_time = now - # 2. Exchange Sync (15s) if now - last_exchange_update >= 15: await self.update_exchange_data() last_exchange_update = now - # 3. DB Sync (5s) candles = await self.db.get_candles(self.db_symbol, self.db_interval, limit=100) if candles: latest = candles[0]