From 8fe822476200795353415cb75f7594aa83f10790 Mon Sep 17 00:00:00 2001 From: Gemini CLI Date: Thu, 5 Mar 2026 23:45:09 +0100 Subject: [PATCH] fix: implement precise rounding for spot asset swaps (v1.5.7) --- src/strategies/ping_pong_bot.py | 53 +++++++++++++++++---------------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/src/strategies/ping_pong_bot.py b/src/strategies/ping_pong_bot.py index acf5c17..b8bda83 100644 --- a/src/strategies/ping_pong_bot.py +++ b/src/strategies/ping_pong_bot.py @@ -86,7 +86,7 @@ class DatabaseManager: class PingPongBot: def __init__(self, config_path="config/ping_pong_config.yaml"): - self.version = "1.5.6" + self.version = "1.5.7" with open(config_path, 'r') as f: self.config = yaml.safe_load(f) @@ -213,10 +213,6 @@ class PingPongBot: if self.direction is not None: await self.close_all_positions() - # Update settings before swap to ensure we trade the right symbol/category if needed - old_symbol = self.symbol - old_category = self.category - self.direction = new_direction if self.direction == "long": self.category = "inverse" @@ -241,6 +237,7 @@ 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) @@ -253,7 +250,7 @@ class PingPongBot: logger.error(f"Error closing positions: {e}") async def swap_assets(self, target_direction): - """Point II: Exchange BTC/USDC on Spot market using UNIFIED account type""" + """Point II: Exchange BTC/USDC on Spot market with proper rounding""" try: logger.info(f"Swapping assets for {target_direction.upper()} mode...") spot_symbol = f"{self.base_coin}USDC" @@ -265,26 +262,27 @@ class PingPongBot: logger.info(f"Current Balances: {coins}") if target_direction == "short": - # SHORT: Need USDC, Sell BTC - btc_bal = coins.get(self.base_coin, 0) - if btc_bal > 0.0001: + # SHORT: Need USDC, Sell BTC (Max 6 decimals for BTCUSDC spot) + btc_bal = floor(coins.get(self.base_coin, 0) * 1000000) / 1000000 + if btc_bal > 0.000001: logger.info(f"Spot: Selling {btc_bal} {self.base_coin} for USDC") res = await asyncio.to_thread(self.session.place_order, - category="spot", symbol=spot_symbol, side="Sell", orderType="Market", qty=str(btc_bal) + category="spot", symbol=spot_symbol, side="Sell", orderType="Market", qty=f"{btc_bal:.6f}" ) logger.info(f"Swap Result: {res['retMsg']}") else: - # LONG: Need BTC, Buy BTC with USDC - usdc_bal = coins.get("USDC", 0) + # LONG: Need BTC, Buy BTC with USDC (Max 4 decimals for USDC amount) + usdc_bal = floor(coins.get("USDC", 0) * 10000) / 10000 if usdc_bal > 1.0: logger.info(f"Spot: Buying {self.base_coin} with {usdc_bal} USDC") + # marketUnit='quote' means spending USDC res = await asyncio.to_thread(self.session.place_order, category="spot", symbol=spot_symbol, side="Buy", orderType="Market", - qty=str(usdc_bal), marketUnit="quote" + qty=f"{usdc_bal:.4f}", marketUnit="quote" ) logger.info(f"Swap Result: {res['retMsg']}") - await asyncio.sleep(3) # Wait for spot settlement + await asyncio.sleep(5) # Wait for spot settlement except Exception as e: logger.error(f"Asset Swap Error: {e}") @@ -295,12 +293,15 @@ class PingPongBot: if ticker['retCode'] == 0: self.market_price = float(ticker['result']['list'][0]['lastPrice']) - 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) + # 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) 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 - wallet = await asyncio.to_thread(self.session.get_wallet_balance, category=self.category, accountType="UNIFIED", coin=self.settle_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'] if res_list: @@ -311,17 +312,18 @@ class PingPongBot: def check_signals(self, df): if len(df) < 2: return None last, prev = df.iloc[-1], df.iloc[-2] - rsi_cfg, hurst_cfg = self.config['rsi'], self.config['hurst'] + rsi_cfg, hurst_cfg = self.config['rsi'] or {}, self.config['hurst'] or {} - l_open = (rsi_cfg['enabled_for_open'] and prev['rsi'] < rsi_cfg['oversold'] and last['rsi'] >= rsi_cfg['oversold']) or \ - (hurst_cfg['enabled_for_open'] and prev['close'] > prev['hurst_lower'] and last['close'] <= last['hurst_lower']) - l_close = (rsi_cfg['enabled_for_close'] and prev['rsi'] > rsi_cfg['overbought'] and last['rsi'] <= rsi_cfg['overbought']) or \ - (hurst_cfg['enabled_for_close'] and prev['close'] < prev['hurst_upper'] and last['close'] >= last['hurst_upper']) + # Signals defined by crossover + l_open = (rsi_cfg.get('enabled_for_open') and prev['rsi'] < rsi_cfg.get('oversold', 30) and last['rsi'] >= rsi_cfg.get('oversold', 30)) or \ + (hurst_cfg.get('enabled_for_open') and prev['close'] > prev['hurst_lower'] and last['close'] <= last['hurst_lower']) + l_close = (rsi_cfg.get('enabled_for_close') and prev['rsi'] > rsi_cfg.get('overbought', 70) and last['rsi'] <= rsi_cfg.get('overbought', 70)) or \ + (hurst_cfg.get('enabled_for_close') and prev['close'] < prev['hurst_upper'] and last['close'] >= last['hurst_upper']) - s_open = (rsi_cfg['enabled_for_open'] and prev['rsi'] > rsi_cfg['overbought'] and last['rsi'] <= rsi_cfg['overbought']) or \ - (hurst_cfg['enabled_for_open'] and prev['close'] < prev['hurst_upper'] and last['close'] >= last['hurst_upper']) - s_close = (rsi_cfg['enabled_for_close'] and prev['rsi'] < rsi_cfg['oversold'] and last['rsi'] >= rsi_cfg['oversold']) or \ - (hurst_cfg['enabled_for_close'] and prev['close'] > prev['hurst_lower'] and last['close'] <= last['hurst_lower']) + s_open = (rsi_cfg.get('enabled_for_open') and prev['rsi'] > rsi_cfg.get('overbought', 70) and last['rsi'] <= rsi_cfg.get('overbought', 70)) or \ + (hurst_cfg.get('enabled_for_open') and prev['close'] < prev['hurst_upper'] and last['close'] >= last['hurst_upper']) + s_close = (rsi_cfg.get('enabled_for_close') and prev['rsi'] < rsi_cfg.get('oversold', 30) and last['rsi'] >= rsi_cfg.get('oversold', 30)) or \ + (hurst_cfg.get('enabled_for_close') and prev['close'] > prev['hurst_lower'] and last['close'] <= last['hurst_lower']) if self.direction == 'long': return "open" if l_open else ("close" if l_close else None) @@ -441,6 +443,7 @@ class PingPongBot: await asyncio.sleep(5) +from math import floor if __name__ == "__main__": bot = PingPongBot() asyncio.run(bot.run())