feat: add real-time status dashboard using rich tables
This commit is contained in:
@ -10,8 +10,13 @@ import pandas as pd
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.table import Table
|
||||||
|
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 to import pybit
|
||||||
try:
|
try:
|
||||||
from pybit.unified_trading import HTTP
|
from pybit.unified_trading import HTTP
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@ -55,12 +60,17 @@ class PingPongBot:
|
|||||||
|
|
||||||
# State
|
# State
|
||||||
self.last_candle_time = None
|
self.last_candle_time = None
|
||||||
self.current_indicators = {}
|
self.current_indicators = {
|
||||||
|
"rsi": {"value": 0.0, "timestamp": "N/A"},
|
||||||
|
"hurst_lower": {"value": 0.0, "timestamp": "N/A"},
|
||||||
|
"hurst_upper": {"value": 0.0, "timestamp": "N/A"}
|
||||||
|
}
|
||||||
self.position = None
|
self.position = None
|
||||||
self.wallet_balance = 0
|
self.wallet_balance = 0
|
||||||
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.console = Console()
|
||||||
|
|
||||||
# Grid parameters from config
|
# Grid parameters from config
|
||||||
self.tp_pct = self.config['take_profit_pct'] / 100.0
|
self.tp_pct = self.config['take_profit_pct'] / 100.0
|
||||||
@ -97,27 +107,80 @@ class PingPongBot:
|
|||||||
mcl = mcl_t / 2
|
mcl = mcl_t / 2
|
||||||
mcl_2 = int(round(mcl / 2))
|
mcl_2 = int(round(mcl / 2))
|
||||||
|
|
||||||
# True Range
|
|
||||||
df['h_l'] = df['high'] - df['low']
|
df['h_l'] = df['high'] - df['low']
|
||||||
df['h_pc'] = abs(df['high'] - df['close'].shift(1))
|
df['h_pc'] = abs(df['high'] - df['close'].shift(1))
|
||||||
df['l_pc'] = abs(df['low'] - df['close'].shift(1))
|
df['l_pc'] = abs(df['low'] - df['close'].shift(1))
|
||||||
df['tr'] = df[['h_l', 'h_pc', 'l_pc']].max(axis=1)
|
df['tr'] = df[['h_l', 'h_pc', 'l_pc']].max(axis=1)
|
||||||
|
|
||||||
# RMA of Close and ATR
|
|
||||||
df['ma_mcl'] = self.rma(df['close'], mcl)
|
df['ma_mcl'] = self.rma(df['close'], mcl)
|
||||||
df['atr_mcl'] = self.rma(df['tr'], mcl)
|
df['atr_mcl'] = self.rma(df['tr'], mcl)
|
||||||
|
|
||||||
# Historical Offset
|
|
||||||
df['center'] = df['ma_mcl'].shift(mcl_2)
|
df['center'] = df['ma_mcl'].shift(mcl_2)
|
||||||
# Fill first values where shift produces NaN with the MA itself (as done in JS: historical_ma || src)
|
|
||||||
df['center'] = df['center'].fillna(df['ma_mcl'])
|
df['center'] = df['center'].fillna(df['ma_mcl'])
|
||||||
|
|
||||||
mcm_off = mcm * df['atr_mcl']
|
mcm_off = mcm * df['atr_mcl']
|
||||||
df['hurst_upper'] = df['center'] + mcm_off
|
df['hurst_upper'] = df['center'] + mcm_off
|
||||||
df['hurst_lower'] = df['center'] - mcm_off
|
df['hurst_lower'] = df['center'] - mcm_off
|
||||||
|
|
||||||
|
# Update current indicator state
|
||||||
|
last_row = df.iloc[-1]
|
||||||
|
now_str = datetime.now().strftime("%H:%M:%S")
|
||||||
|
self.current_indicators["rsi"] = {"value": last_row['rsi'], "timestamp": now_str}
|
||||||
|
self.current_indicators["hurst_lower"] = {"value": last_row['hurst_lower'], "timestamp": now_str}
|
||||||
|
self.current_indicators["hurst_upper"] = {"value": last_row['hurst_upper'], "timestamp": now_str}
|
||||||
|
|
||||||
return df
|
return df
|
||||||
|
|
||||||
|
def render_dashboard(self):
|
||||||
|
"""Render a clean summary of the bot state"""
|
||||||
|
# We don't clear to avoid flickering in some terminals, just print a separator or use rich.Live if needed
|
||||||
|
# But for simple docker logs, clear is usually fine or just printing the table.
|
||||||
|
|
||||||
|
# 1. Config Table
|
||||||
|
cfg_table = Table(title="[bold cyan]PING-PONG BOT CONFIG[/]", box=box.ROUNDED, expand=True)
|
||||||
|
cfg_table.add_column("Property", style="dim")
|
||||||
|
cfg_table.add_column("Value", style="bold white")
|
||||||
|
cfg_table.add_row("Symbol", f"{self.symbol} ({self.interval}m)")
|
||||||
|
cfg_table.add_row("Direction", f"{self.direction.upper()}")
|
||||||
|
cfg_table.add_row("Capital/Margin", f"${self.pos_size_margin} (Lev: {self.leverage}x)")
|
||||||
|
cfg_table.add_row("Partial Exit", f"{self.partial_exit_pct*100}%")
|
||||||
|
cfg_table.add_row("TP Target", f"{self.tp_pct*100}%")
|
||||||
|
|
||||||
|
# 2. Indicators Table
|
||||||
|
ind_table = Table(title="[bold yellow]TECHNICAL INDICATORS[/]", box=box.ROUNDED, expand=True)
|
||||||
|
ind_table.add_column("Indicator", style="dim")
|
||||||
|
ind_table.add_column("Current Value", justify="right")
|
||||||
|
ind_table.add_column("Last Update", justify="center")
|
||||||
|
ind_table.add_row("RSI", f"{self.current_indicators['rsi']['value']:.2f}", self.current_indicators['rsi']['timestamp'])
|
||||||
|
ind_table.add_row("Hurst Lower", f"{self.current_indicators['hurst_lower']['value']:.2f}", self.current_indicators['hurst_lower']['timestamp'])
|
||||||
|
ind_table.add_row("Hurst Upper", f"{self.current_indicators['hurst_upper']['value']:.2f}", self.current_indicators['hurst_upper']['timestamp'])
|
||||||
|
|
||||||
|
# 3. Position Table
|
||||||
|
pos_table = Table(title="[bold green]ACTIVE POSITION & ACCOUNT[/]", box=box.ROUNDED, expand=True)
|
||||||
|
pos_table.add_column("Property", style="dim")
|
||||||
|
pos_table.add_column("Value", style="bold white")
|
||||||
|
pos_table.add_row("Wallet Balance", f"${self.wallet_balance:.2f}")
|
||||||
|
|
||||||
|
if self.position:
|
||||||
|
p = self.position
|
||||||
|
pnl = float(p.get('unrealisedPnl', 0))
|
||||||
|
pnl_color = "green" if pnl >= 0 else "red"
|
||||||
|
pos_table.add_row("Position Size", f"{p['size']}")
|
||||||
|
pos_table.add_row("Entry Price", f"{p['avgPrice']}")
|
||||||
|
pos_table.add_row("Unrealized PnL", f"[{pnl_color}]${pnl:.2f}[/]")
|
||||||
|
pos_table.add_row("Liquidation", f"{p.get('liqPrice', 'N/A')}")
|
||||||
|
else:
|
||||||
|
pos_table.add_row("Position", "NONE (Scanning...)")
|
||||||
|
|
||||||
|
pos_table.add_row("Last Signal", str(self.last_signal or "None"))
|
||||||
|
pos_table.add_row("Status", f"[bold blue]{self.status_msg}[/]")
|
||||||
|
|
||||||
|
self.console.print("\n" + "="*50)
|
||||||
|
self.console.print(cfg_table)
|
||||||
|
self.console.print(ind_table)
|
||||||
|
self.console.print(pos_table)
|
||||||
|
self.console.print(f"[dim]Uptime: {str(datetime.now() - self.start_time).split('.')[0]} | Last Run: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}[/]\n")
|
||||||
|
|
||||||
async def fetch_data(self):
|
async def fetch_data(self):
|
||||||
"""Fetch latest Klines from Bybit V5"""
|
"""Fetch latest Klines from Bybit V5"""
|
||||||
try:
|
try:
|
||||||
@ -149,7 +212,7 @@ class PingPongBot:
|
|||||||
async def update_account_info(self):
|
async def update_account_info(self):
|
||||||
"""Update position and balance information"""
|
"""Update position and balance information"""
|
||||||
try:
|
try:
|
||||||
# Get Position
|
# Get Position (with settleCoin for Hedge Mode compatibility)
|
||||||
pos_response = self.session.get_positions(
|
pos_response = self.session.get_positions(
|
||||||
category="linear",
|
category="linear",
|
||||||
symbol=self.symbol,
|
symbol=self.symbol,
|
||||||
@ -333,22 +396,16 @@ class PingPongBot:
|
|||||||
df = await self.fetch_data()
|
df = await self.fetch_data()
|
||||||
|
|
||||||
if df is not None:
|
if df is not None:
|
||||||
# 3. Check for New Candle (for signal processing)
|
# 3. Strategy Logic
|
||||||
last_price = float(df.iloc[-1]['close'])
|
|
||||||
|
|
||||||
# 4. Strategy Logic
|
|
||||||
signal = self.check_signals(df)
|
signal = self.check_signals(df)
|
||||||
if signal:
|
if signal:
|
||||||
logger.info(f"Signal detected: {signal} @ {last_price}")
|
logger.info(f"Signal detected: {signal} @ {df.iloc[-1]['close']}")
|
||||||
await self.execute_trade_logic(df, signal)
|
await self.execute_trade_logic(df, signal)
|
||||||
|
|
||||||
# 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))
|
# 4. Render Summary Dashboard
|
||||||
|
self.render_dashboard()
|
||||||
|
|
||||||
|
await asyncio.sleep(self.config.get('loop_interval_seconds', 10))
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
try:
|
try:
|
||||||
@ -357,5 +414,4 @@ if __name__ == "__main__":
|
|||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\nBot Stopped by User")
|
print("\nBot Stopped by User")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"\nCritical Error: {e}")
|
|
||||||
logger.exception("Critical Error in main loop")
|
logger.exception("Critical Error in main loop")
|
||||||
|
|||||||
Reference in New Issue
Block a user