feat: fix Bybit Unified Account support and enhance Docker logging for ping_pong_bot

- Added accountType='UNIFIED' to wallet balance requests
- Updated balance retrieval to support totalWalletBalance for UTA
- Replaced rich.Live with standard logging for better Docker compatibility
- Added PYTHONUNBUFFERED=1 to ensure real-time logs in containers
- Updated docker-compose to point to NAS database (20.20.20.20)
- Created GEMINI.md with comprehensive project context
This commit is contained in:
Gemini CLI
2026-03-05 11:04:30 +01:00
parent 30aeda0901
commit da7fbd1b49
5 changed files with 151 additions and 105 deletions

View File

@ -10,12 +10,6 @@ import pandas as pd
import numpy as np
from datetime import datetime, timezone
from dotenv import load_dotenv
from rich.console import Console
from rich.table import Table
from rich.live import Live
from rich.panel import Panel
from rich.layout import Layout
from rich import box
# Try to import pybit, if not available, we'll suggest installing it
try:
@ -26,17 +20,18 @@ except ImportError:
# Load environment variables
load_dotenv()
log_level = os.getenv("LOG_LEVEL", "INFO")
# Setup Logging
logging.basicConfig(
level=logging.INFO,
level=getattr(logging, log_level),
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
filename='logs/ping_pong_bot.log'
handlers=[
logging.StreamHandler()
]
)
logger = logging.getLogger("PingPongBot")
console = Console()
class PingPongBot:
def __init__(self, config_path="config/ping_pong_config.yaml"):
with open(config_path, 'r') as f:
@ -162,7 +157,6 @@ class PingPongBot:
if pos_response['retCode'] == 0:
positions = pos_response['result']['list']
# Filter by side or just take the one with size > 0
active_pos = [p for p in positions if float(p['size']) > 0]
if active_pos:
self.position = active_pos[0]
@ -172,11 +166,23 @@ class PingPongBot:
# Get Balance
wallet_response = self.session.get_wallet_balance(
category="linear",
accountType="UNIFIED",
coin="USDT"
)
if wallet_response['retCode'] == 0:
self.wallet_balance = float(wallet_response['result']['list'][0]['coin'][0]['walletBalance'])
result_list = wallet_response['result']['list']
if result_list:
# Priority 1: totalWalletBalance (for UTA pooled funds)
self.wallet_balance = float(result_list[0].get('totalWalletBalance', 0))
# If totalWalletBalance is 0, check the specific coin
if self.wallet_balance == 0:
coin_info = result_list[0].get('coin', [])
if coin_info:
self.wallet_balance = float(coin_info[0].get('walletBalance', 0))
else:
logger.error(f"Wallet API Error: {wallet_response['retMsg']}")
except Exception as e:
logger.error(f"Error updating account info: {e}")
@ -311,98 +317,40 @@ class PingPongBot:
logger.error(f"Execution Error: {e}")
self.status_msg = f"Exec Error: {str(e)}"
def create_dashboard(self, df):
"""Create a Rich layout for status display"""
layout = Layout()
layout.split_column(
Layout(name="header", size=3),
Layout(name="main", ratio=1),
Layout(name="footer", size=3)
)
# Header
header_table = Table.grid(expand=True)
header_table.add_column(justify="left", ratio=1)
header_table.add_column(justify="right", ratio=1)
runtime = str(datetime.now() - self.start_time).split('.')[0]
header_table.add_row(
f"[bold cyan]Ping-Pong Bot v1.0[/bold cyan] | Symbol: [yellow]{self.symbol}[/yellow] | TF: [yellow]{self.interval}m[/yellow]",
f"Runtime: [green]{runtime}[/green] | Time: {datetime.now().strftime('%H:%M:%S')}"
)
layout["header"].update(Panel(header_table, style="white on blue"))
# Main Content
main_table = Table(box=box.SIMPLE, expand=True)
main_table.add_column("Category", style="cyan")
main_table.add_column("Value", style="white")
# Indicators
last = df.iloc[-1]
rsi_val = f"{last['rsi']:.2f}"
rsi_status = "[green]Oversold[/green]" if last['rsi'] < self.config['rsi']['oversold'] else ("[red]Overbought[/red]" if last['rsi'] > self.config['rsi']['overbought'] else "Neutral")
main_table.add_row("Price", f"{last['close']:.2f}")
main_table.add_row("RSI", f"{rsi_val} ({rsi_status})")
main_table.add_row("Hurst Upper", f"{last['hurst_upper']:.2f}")
main_table.add_row("Hurst Lower", f"{last['hurst_lower']:.2f}")
main_table.add_section()
# Position Info
if self.position:
size = self.position['size']
avg_p = self.position['avgPrice']
upnl = float(self.position['unrealisedPnl'])
upnl_style = "green" if upnl >= 0 else "red"
main_table.add_row("Position Size", f"{size}")
main_table.add_row("Avg Entry", f"{avg_p}")
main_table.add_row("Unrealized PnL", f"[{upnl_style}]{upnl:.2f} USDT[/{upnl_style}]")
else:
main_table.add_row("Position", "None")
main_table.add_row("Wallet Balance", f"{self.wallet_balance:.2f} USDT")
layout["main"].update(Panel(main_table, title="Current Status", border_style="cyan"))
# Footer
footer_text = f"Status: [bold white]{self.status_msg}[/bold white]"
if self.last_signal:
footer_text += f" | Last Action: [yellow]{self.last_signal}[/yellow]"
layout["footer"].update(Panel(footer_text, border_style="yellow"))
return layout
async def run(self):
"""Main loop"""
with Live(console=console, refresh_per_second=1) as live:
while True:
# 1. Update Account
await self.update_account_info()
logger.info(f"Bot started for {self.symbol} in {self.direction} mode")
while True:
# 1. Update Account
await self.update_account_info()
# 2. Fetch Data & Calculate Indicators
df = await self.fetch_data()
if df is not None:
# 3. Check for New Candle (for signal processing)
last_price = float(df.iloc[-1]['close'])
# 2. Fetch Data & Calculate Indicators
df = await self.fetch_data()
# 4. Strategy Logic
signal = self.check_signals(df)
if signal:
logger.info(f"Signal detected: {signal} @ {last_price}")
await self.execute_trade_logic(df, signal)
if df is not None:
# 3. Check for New Candle (for signal processing)
current_time = df.iloc[-1]['start_time']
# 4. Strategy Logic
signal = self.check_signals(df)
await self.execute_trade_logic(df, signal)
# 5. Update UI
live.update(self.create_dashboard(df))
await asyncio.sleep(self.config.get('loop_interval_seconds', 5))
# 5. Simple status log
if self.position:
logger.info(f"Price: {last_price:.2f} | Pos: {self.position['size']} @ {self.position['avgPrice']} | Wallet: {self.wallet_balance:.2f}")
else:
logger.info(f"Price: {last_price:.2f} | No Position | Wallet: {self.wallet_balance:.2f}")
await asyncio.sleep(self.config.get('loop_interval_seconds', 5))
if __name__ == "__main__":
try:
bot = PingPongBot()
asyncio.run(bot.run())
except KeyboardInterrupt:
console.print("\n[bold red]Bot Stopped by User[/bold red]")
print("\nBot Stopped by User")
except Exception as e:
console.print(f"\n[bold red]Critical Error: {e}[/bold red]")
print(f"\nCritical Error: {e}")
logger.exception("Critical Error in main loop")