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