feat: use rich.Live(screen=False) for flicker-free dashboard (v1.3.9)

This commit is contained in:
Gemini CLI
2026-03-05 22:42:48 +01:00
parent 08a4639dad
commit b44847cbbd

View File

@ -15,6 +15,7 @@ from rich.console import Console, Group
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
@ -40,7 +41,6 @@ logging.basicConfig(
logger = logging.getLogger("PingPongBot")
class DatabaseManager:
"""Minimal Database Manager for the bot"""
def __init__(self):
self.host = os.getenv('DB_HOST', '20.20.20.20')
self.port = int(os.getenv('DB_PORT', 5433))
@ -68,7 +68,7 @@ class DatabaseManager:
class PingPongBot:
def __init__(self, config_path="config/ping_pong_config.yaml"):
self.version = "1.3.8"
self.version = "1.3.9"
with open(config_path, 'r') as f:
self.config = yaml.safe_load(f)
@ -78,19 +78,19 @@ 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")
# Corrected timeout parameter for pybit V5 HTTP
self.session = HTTP(
testnet=False,
api_key=self.api_key,
api_secret=self.api_secret
api_secret=self.api_secret,
timeout=10
)
self.db = DatabaseManager()
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
self.direction = self.config['direction'].lower()
# State
@ -168,11 +168,11 @@ class PingPongBot:
wallet = await asyncio.to_thread(self.session.get_wallet_balance, category="linear", accountType="UNIFIED", coin="USDT")
if wallet['retCode'] == 0:
result_list = wallet['result']['list']
if result_list:
self.wallet_balance = float(result_list[0].get('totalWalletBalance', 0))
res_list = wallet['result']['list']
if res_list:
self.wallet_balance = float(res_list[0].get('totalWalletBalance', 0))
if self.wallet_balance == 0:
coin_info = result_list[0].get('coin', [])
coin_info = res_list[0].get('coin', [])
if coin_info:
self.wallet_balance = float(coin_info[0].get('walletBalance', 0))
except Exception as e:
@ -231,90 +231,79 @@ class PingPongBot:
except Exception as e:
logger.error(f"Trade Error: {e}")
def get_dashboard_content(self):
"""Prepares the dashboard content as a Group of tables"""
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def get_dashboard(self):
"""Generates a single consolidated Panel for the dashboard"""
now = datetime.now().strftime("%H:%M:%S")
# 1. Header
header = Table(title=f"PING-PONG BOT v{self.version} [{self.direction.upper()}]", 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("Last Candle", f"{self.last_candle_time} (@${self.last_candle_price:,.2f})")
# 1. Info Table
info = Table(box=None, expand=True, show_header=False)
info.add_column("K", style="cyan"); info.add_column("V", style="white")
info.add_row("Symbol", f"{self.symbol} [{self.direction.upper()}]")
info.add_row("Price", f"${self.market_price:,.2f}")
info.add_row("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'])
# 2. Indicators Table
inds = Table(box=box.SIMPLE, expand=True)
inds.add_column("Indicator", style="dim"); inds.add_column("Value", justify="right"); inds.add_column("Updated", justify="center")
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'])
# 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}")
# 3. Position Table
pos = Table(box=box.SIMPLE, expand=True)
pos.add_column("Wallet", justify="center"); pos.add_column("Size", justify="center"); pos.add_column("PnL", justify="center")
if self.position:
pnl = float(self.position['unrealisedPnl'])
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}")
pos.add_row(f"${self.wallet_balance:,.2f}", str(self.position['size']), f"[bold {'green' if pnl>=0 else 'red'}]${pnl:,.2f}")
else:
pos.add_row("Position", "NONE")
pos.add_row(f"${self.wallet_balance:,.2f}", "0", "-")
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}[/]")
return Group(header, inds, pos)
content = Group(info, inds, pos, f"[dim]Status: {self.status_msg} | Signal: {self.last_signal or 'None'} | Heartbeat: {now}[/]")
return Panel(content, title=f"[bold cyan]PING-PONG BOT v{self.version}[/]", border_style="blue")
async def run(self):
logger.info("Bot starting...")
logger.info(f"Bot v{self.version} starting...")
await self.db.connect()
last_exchange_update = 0
# Clear screen once at start
self.console.clear()
while True:
try:
now_ts = time.time()
with Live(self.get_dashboard(), console=self.console, refresh_per_second=1, screen=False) as live:
while True:
try:
now_ts = time.time()
# 1. Exchange Sync (15s)
if now_ts - last_exchange_update >= 15:
await asyncio.wait_for(self.update_exchange_data(), timeout=10)
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: {signal}")
await self.execute_trade(signal)
self.last_candle_time = latest['time']
self.last_candle_price = latest['close']
self.status_msg = f"Last Candle: {latest['time'].strftime('%H:%M:%S')}"
else:
self.status_msg = "Waiting for candles..."
# 3. Update Dashboard
live.update(self.get_dashboard())
except asyncio.TimeoutError:
self.status_msg = "Exchange Timeout"
except Exception as e:
logger.error(f"Loop error: {e}")
self.status_msg = f"Error: {str(e)[:40]}"
# 1. Exchange Sync (15s)
if now_ts - last_exchange_update >= 15:
await asyncio.wait_for(self.update_exchange_data(), timeout=10)
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'].strftime('%H:%M:%S')}"
else:
self.status_msg = f"No candles found for {self.db_symbol}/{self.db_interval}"
# 3. Print Dashboard - Overwrite using ANSI
# Moving cursor to top-left (0,0) and printing the group
# This is much smoother than clear()
self.console.print("\033[H", end="")
self.console.print(self.get_dashboard_content())
except asyncio.TimeoutError:
self.status_msg = "Exchange Sync Timeout"
except Exception as e:
logger.exception(f"Loop Error: {e}")
self.status_msg = f"Error: {str(e)[:50]}"
await asyncio.sleep(5)
await asyncio.sleep(5)
if __name__ == "__main__":
bot = PingPongBot()