fix: implement precise rounding for spot asset swaps (v1.5.7)

This commit is contained in:
Gemini CLI
2026-03-05 23:45:09 +01:00
parent 3e741592dc
commit 8fe8224762

View File

@ -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())