feat: use ANSI cursor reset for flicker-free dashboard (v1.3.8)
This commit is contained in:
@ -11,7 +11,7 @@ import numpy as np
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import List, Dict, Any, Optional
|
from typing import List, Dict, Any, Optional
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from rich.console import Console
|
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
|
||||||
@ -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.7"
|
self.version = "1.3.8"
|
||||||
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)
|
||||||
|
|
||||||
@ -107,7 +107,6 @@ class PingPongBot:
|
|||||||
self.status_msg = "Initializing..."
|
self.status_msg = "Initializing..."
|
||||||
self.last_signal = None
|
self.last_signal = None
|
||||||
self.start_time = datetime.now()
|
self.start_time = datetime.now()
|
||||||
self.last_heartbeat = datetime.now()
|
|
||||||
self.console = Console()
|
self.console = Console()
|
||||||
|
|
||||||
# Parameters
|
# Parameters
|
||||||
@ -157,9 +156,7 @@ class PingPongBot:
|
|||||||
return df
|
return df
|
||||||
|
|
||||||
async def update_exchange_data(self):
|
async def update_exchange_data(self):
|
||||||
"""Fetch Price, Balance, Position every 15s with timeout"""
|
|
||||||
try:
|
try:
|
||||||
# Wrap synchronous pybit calls in asyncio.to_thread for better async behavior
|
|
||||||
ticker = await asyncio.to_thread(self.session.get_tickers, category="linear", symbol=self.symbol)
|
ticker = await asyncio.to_thread(self.session.get_tickers, category="linear", symbol=self.symbol)
|
||||||
if ticker['retCode'] == 0:
|
if ticker['retCode'] == 0:
|
||||||
self.market_price = float(ticker['result']['list'][0]['lastPrice'])
|
self.market_price = float(ticker['result']['list'][0]['lastPrice'])
|
||||||
@ -234,17 +231,15 @@ class PingPongBot:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Trade Error: {e}")
|
logger.error(f"Trade Error: {e}")
|
||||||
|
|
||||||
def print_dashboard(self):
|
def get_dashboard_content(self):
|
||||||
"""Prints a clean summary to the logs"""
|
"""Prepares the dashboard content as a Group of tables"""
|
||||||
now = datetime.now().strftime("%H:%M:%S")
|
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
self.last_heartbeat = datetime.now()
|
|
||||||
|
|
||||||
# 1. Header
|
# 1. Header
|
||||||
header = Table(title=f"PING-PONG BOT v{self.version} Dashboard", box=box.ROUNDED, expand=True)
|
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_column("Property"); header.add_column("Value")
|
||||||
header.add_row("Symbol", self.symbol)
|
header.add_row("Symbol", self.symbol)
|
||||||
header.add_row("Market Price", f"${self.market_price:,.2f}")
|
header.add_row("Market Price", f"${self.market_price:,.2f}")
|
||||||
header.add_row("Direction", self.direction.upper())
|
|
||||||
header.add_row("Last Candle", f"{self.last_candle_time} (@${self.last_candle_price:,.2f})")
|
header.add_row("Last Candle", f"{self.last_candle_time} (@${self.last_candle_price:,.2f})")
|
||||||
|
|
||||||
# 2. Indicators
|
# 2. Indicators
|
||||||
@ -264,31 +259,28 @@ class PingPongBot:
|
|||||||
pos.add_row("Entry Price", f"${float(self.position['avgPrice']):,.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}")
|
pos.add_row("Unrealized PnL", f"[bold {'green' if pnl>=0 else 'red'}]${pnl:,.2f}")
|
||||||
else:
|
else:
|
||||||
pos_row = "NONE"
|
|
||||||
pos.add_row("Position", "NONE")
|
pos.add_row("Position", "NONE")
|
||||||
|
|
||||||
pos.add_row("Status", f"[bold blue]{self.status_msg}[/]")
|
pos.add_row("Status", f"[bold blue]{self.status_msg}[/]")
|
||||||
pos.add_row("Last Signal", str(self.last_signal or "None"))
|
pos.add_row("Last Signal", str(self.last_signal or "None"))
|
||||||
pos.add_row("Heartbeat", f"[italic]{now}[/]")
|
pos.add_row("Heartbeat", f"[italic]{now}[/]")
|
||||||
|
|
||||||
self.console.print("\n")
|
return Group(header, inds, pos)
|
||||||
self.console.print(header)
|
|
||||||
self.console.print(inds)
|
|
||||||
self.console.print(pos)
|
|
||||||
self.console.print("-" * 50)
|
|
||||||
|
|
||||||
async def run(self):
|
async def run(self):
|
||||||
logger.info("Bot starting...")
|
logger.info("Bot starting...")
|
||||||
await self.db.connect()
|
await self.db.connect()
|
||||||
last_exchange_update = 0
|
last_exchange_update = 0
|
||||||
|
|
||||||
|
# Clear screen once at start
|
||||||
|
self.console.clear()
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
now_ts = time.time()
|
now_ts = time.time()
|
||||||
|
|
||||||
# 1. Exchange Sync (15s)
|
# 1. Exchange Sync (15s)
|
||||||
if now_ts - last_exchange_update >= 15:
|
if now_ts - last_exchange_update >= 15:
|
||||||
# Wrapped in timeout to prevent hanging
|
|
||||||
await asyncio.wait_for(self.update_exchange_data(), timeout=10)
|
await asyncio.wait_for(self.update_exchange_data(), timeout=10)
|
||||||
last_exchange_update = now_ts
|
last_exchange_update = now_ts
|
||||||
|
|
||||||
@ -306,16 +298,18 @@ class PingPongBot:
|
|||||||
await self.execute_trade(signal)
|
await self.execute_trade(signal)
|
||||||
self.last_candle_time = latest['time']
|
self.last_candle_time = latest['time']
|
||||||
self.last_candle_price = latest['close']
|
self.last_candle_price = latest['close']
|
||||||
self.status_msg = f"New Candle: {latest['time']}"
|
self.status_msg = f"New Candle: {latest['time'].strftime('%H:%M:%S')}"
|
||||||
else:
|
else:
|
||||||
self.status_msg = f"No candles found for {self.db_symbol}/{self.db_interval}"
|
self.status_msg = f"No candles found for {self.db_symbol}/{self.db_interval}"
|
||||||
|
|
||||||
# 3. Print Dashboard
|
# 3. Print Dashboard - Overwrite using ANSI
|
||||||
self.print_dashboard()
|
# 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:
|
except asyncio.TimeoutError:
|
||||||
logger.error("Exchange update timed out")
|
self.status_msg = "Exchange Sync Timeout"
|
||||||
self.status_msg = "Exchange Update Timeout"
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(f"Loop Error: {e}")
|
logger.exception(f"Loop Error: {e}")
|
||||||
self.status_msg = f"Error: {str(e)[:50]}"
|
self.status_msg = f"Error: {str(e)[:50]}"
|
||||||
|
|||||||
Reference in New Issue
Block a user