revert: remove all blinking related Live UI changes (v1.4.1)

This commit is contained in:
Gemini CLI
2026-03-05 22:50:40 +01:00
parent b44847cbbd
commit cf0ccbcad5

View File

@ -11,11 +11,10 @@ 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, Group from rich.console import Console
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
@ -41,6 +40,7 @@ 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.9" self.version = "1.4.1"
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,7 +78,6 @@ 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,
@ -87,10 +86,13 @@ class PingPongBot:
) )
self.db = DatabaseManager() self.db = DatabaseManager()
self.symbol = self.config['symbol'].upper()
self.db_symbol = self.symbol.replace("USDT", "") self.symbol = self.config['symbol'].upper() # e.g. BTCUSDT
self.db_symbol = self.symbol.replace("USDT", "") # e.g. BTC
self.interval = str(self.config['interval']) self.interval = str(self.config['interval'])
# Map interval to DB format: '1' -> '1m'
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
@ -122,16 +124,19 @@ class PingPongBot:
return series.ewm(alpha=alpha, adjust=False).mean() return series.ewm(alpha=alpha, adjust=False).mean()
def calculate_indicators(self, df): def calculate_indicators(self, df):
# RSI
rsi_cfg = self.config['rsi'] rsi_cfg = self.config['rsi']
delta = df['close'].diff() delta = df['close'].diff()
gain = delta.where(delta > 0, 0) gain = delta.where(delta > 0, 0)
loss = -delta.where(delta < 0, 0) loss = -delta.where(delta < 0, 0)
df['rsi'] = 100 - (100 / (1 + (self.rma(gain, rsi_cfg['period']) / self.rma(loss, rsi_cfg['period'])))) df['rsi'] = 100 - (100 / (1 + (self.rma(gain, rsi_cfg['period']) / self.rma(loss, rsi_cfg['period']))))
# Hurst
hurst_cfg = self.config['hurst'] hurst_cfg = self.config['hurst']
mcl = hurst_cfg['period'] / 2 mcl = hurst_cfg['period'] / 2
mcl_2 = int(round(mcl / 2)) mcl_2 = int(round(mcl / 2))
# Vectorized TR calculation
df['tr'] = np.maximum( df['tr'] = np.maximum(
df['high'] - df['low'], df['high'] - df['low'],
np.maximum( np.maximum(
@ -156,23 +161,25 @@ class PingPongBot:
return df return df
async def update_exchange_data(self): async def update_exchange_data(self):
"""Fetch Price, Balance, Position every 15s"""
try: try:
# Wrap synchronous pybit calls in asyncio.to_thread
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'])
pos = await asyncio.to_thread(self.session.get_positions, category="linear", symbol=self.symbol, settleCoin="USDT") pos = await asyncio.to_thread(self.session.get_positions, category="linear", symbol=self.symbol, settleCoin="USDT")
if pos['retCode'] == 0: if pos['retCode'] == 0:
active = [p for p in pos['result']['list'] if float(p['size']) > 0] active = [p for p in pos['result']['list'] if float(p.get('size', 0)) > 0]
self.position = active[0] if active else None self.position = active[0] if active else None
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:
res_list = wallet['result']['list'] result_list = wallet['result']['list']
if res_list: if result_list:
self.wallet_balance = float(res_list[0].get('totalWalletBalance', 0)) self.wallet_balance = float(result_list[0].get('totalWalletBalance', 0))
if self.wallet_balance == 0: if self.wallet_balance == 0:
coin_info = res_list[0].get('coin', []) coin_info = result_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:
@ -182,11 +189,13 @@ class PingPongBot:
last, prev = df.iloc[-1], df.iloc[-2] last, prev = df.iloc[-1], df.iloc[-2]
rsi_cfg, hurst_cfg = self.config['rsi'], self.config['hurst'] rsi_cfg, hurst_cfg = self.config['rsi'], self.config['hurst']
# Long Signals
l_open = (rsi_cfg['enabled_for_open'] and prev['rsi'] < rsi_cfg['oversold'] and last['rsi'] >= rsi_cfg['oversold']) or \ l_open = (rsi_cfg['enabled_for_open'] and prev['rsi'] < rsi_cfg['oversold'] and last['rsi'] >= rsi_cfg['oversold']) or \
(hurst_cfg['enabled_for_open'] and prev['close'] > prev['hurst_lower'] and last['close'] <= last['hurst_lower']) (hurst_cfg['enabled_for_open'] and prev['close'] > prev['hurst_lower'] and last['close'] <= last['hurst_lower'])
l_close = (rsi_cfg['enabled_for_close'] and prev['rsi'] > rsi_cfg['overbought'] and last['rsi'] <= rsi_cfg['overbought']) or \ l_close = (rsi_cfg['enabled_for_close'] and prev['rsi'] > rsi_cfg['overbought'] and last['rsi'] <= rsi_cfg['overbought']) or \
(hurst_cfg['enabled_for_close'] and prev['close'] < prev['hurst_upper'] and last['close'] >= last['hurst_upper']) (hurst_cfg['enabled_for_close'] and prev['close'] < prev['hurst_upper'] and last['close'] >= last['hurst_upper'])
# Short Signals
s_open = (rsi_cfg['enabled_for_open'] and prev['rsi'] > rsi_cfg['overbought'] and last['rsi'] <= rsi_cfg['overbought']) or \ s_open = (rsi_cfg['enabled_for_open'] and prev['rsi'] > rsi_cfg['overbought'] and last['rsi'] <= rsi_cfg['overbought']) or \
(hurst_cfg['enabled_for_open'] and prev['close'] < prev['hurst_upper'] and last['close'] >= last['hurst_upper']) (hurst_cfg['enabled_for_open'] and prev['close'] < prev['hurst_upper'] and last['close'] >= last['hurst_upper'])
s_close = (rsi_cfg['enabled_for_close'] and prev['rsi'] < rsi_cfg['oversold'] and last['rsi'] >= rsi_cfg['oversold']) or \ s_close = (rsi_cfg['enabled_for_close'] and prev['rsi'] < rsi_cfg['oversold'] and last['rsi'] >= rsi_cfg['oversold']) or \
@ -231,79 +240,64 @@ 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(self): def render_dashboard(self):
"""Generates a single consolidated Panel for the dashboard""" # standard print based dashboard
now = datetime.now().strftime("%H:%M:%S") self.console.print("\n" + "="*60)
cfg_table = Table(title=f"PING-PONG BOT v{self.version} [{self.direction.upper()}]", box=box.ROUNDED, expand=True)
cfg_table.add_column("Property"); cfg_table.add_column("Value")
cfg_table.add_row("Symbol", self.symbol); cfg_table.add_row("Price", f"${self.market_price:.2f}")
cfg_table.add_row("Last Candle", f"{self.last_candle_time} (@${self.last_candle_price:.2f})")
# 1. Info Table ind_table = Table(title="INDICATORS", box=box.ROUNDED, expand=True)
info = Table(box=None, expand=True, show_header=False) ind_table.add_column("Indicator"); ind_table.add_column("Value"); ind_table.add_column("Updated")
info.add_column("K", style="cyan"); info.add_column("V", style="white") for k, v in self.current_indicators.items():
info.add_row("Symbol", f"{self.symbol} [{self.direction.upper()}]") ind_table.add_row(k.upper(), f"{v['value']:.2f}", v['timestamp'])
info.add_row("Price", f"${self.market_price:,.2f}")
info.add_row("Candle", f"{self.last_candle_time} (@${self.last_candle_price:,.2f})") pos_table = Table(title="POSITION", box=box.ROUNDED, expand=True)
pos_table.add_column("Wallet"); pos_table.add_column("Size"); pos_table.add_column("Entry"); pos_table.add_column("PnL")
# 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 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: if self.position:
pnl = float(self.position['unrealisedPnl']) pnl = float(self.position['unrealisedPnl'])
pos.add_row(f"${self.wallet_balance:,.2f}", str(self.position['size']), f"[bold {'green' if pnl>=0 else 'red'}]${pnl:,.2f}") pos_table.add_row(f"${self.wallet_balance:.2f}", self.position['size'], self.position['avgPrice'], f"[bold {'green' if pnl>=0 else 'red'}]${pnl:.2f}")
else: else:
pos.add_row(f"${self.wallet_balance:,.2f}", "0", "-") pos_table.add_row(f"${self.wallet_balance:.2f}", "0", "-", "-")
content = Group(info, inds, pos, f"[dim]Status: {self.status_msg} | Signal: {self.last_signal or 'None'} | Heartbeat: {now}[/]") self.console.print(cfg_table); self.console.print(ind_table); self.console.print(pos_table)
return Panel(content, title=f"[bold cyan]PING-PONG BOT v{self.version}[/]", border_style="blue") self.console.print(f"[dim]Status: {self.status_msg} | Signal: {self.last_signal}[/]")
self.console.print("="*60 + "\n")
async def run(self): async def run(self):
logger.info(f"Bot v{self.version} starting...")
await self.db.connect() await self.db.connect()
last_exchange_update = 0 last_exchange_update = 0
while True:
with Live(self.get_dashboard(), console=self.console, refresh_per_second=1, screen=False) as live: try:
while True: now = time.time()
try: # 1. Exchange Sync (15s)
now_ts = time.time() if now - last_exchange_update >= 15:
await self.update_exchange_data()
# 1. Exchange Sync (15s) last_exchange_update = now
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]}"
await asyncio.sleep(5) # 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: 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']}"
else:
self.status_msg = f"No candles found for {self.db_symbol}/{self.db_interval}"
self.render_dashboard()
except Exception as e:
logger.error(f"Loop error: {e}")
self.status_msg = f"Error: {str(e)[:40]}"
await asyncio.sleep(5)
if __name__ == "__main__": if __name__ == "__main__":
bot = PingPongBot() bot = PingPongBot()