feat: replace rich.live with standard print and add exchange timeouts (v1.3.6)

This commit is contained in:
Gemini CLI
2026-03-05 22:36:16 +01:00
parent f1d7c7138c
commit fb9736177a

View File

@ -15,7 +15,6 @@ 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
@ -69,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.5" self.version = "1.3.6"
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)
@ -79,16 +78,18 @@ 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")
# Added timeout to prevent hanging
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,
request_timeout=10
) )
self.db = DatabaseManager() self.db = DatabaseManager()
self.symbol = self.config['symbol'].upper() # e.g. BTCUSDT self.symbol = self.config['symbol'].upper()
self.db_symbol = self.symbol.replace("USDT", "") # e.g. BTC 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
@ -108,7 +109,7 @@ 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_refresh_time = datetime.now() self.last_heartbeat = datetime.now()
self.console = Console() self.console = Console()
# Parameters # Parameters
@ -124,14 +125,12 @@ 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))
@ -160,7 +159,6 @@ 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:
ticker = self.session.get_tickers(category="linear", symbol=self.symbol) ticker = self.session.get_tickers(category="linear", symbol=self.symbol)
if ticker['retCode'] == 0: if ticker['retCode'] == 0:
@ -236,60 +234,61 @@ class PingPongBot:
except Exception as e: except Exception as e:
logger.error(f"Trade Error: {e}") logger.error(f"Trade Error: {e}")
def generate_dashboard(self): def print_dashboard(self):
"""Generates the dashboard layout""" """Prints a clean summary to the logs (replaces Live which can hang in Docker)"""
layout = Layout() now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
layout.split_column( self.last_heartbeat = datetime.now()
Layout(name="header", size=3),
Layout(name="indicators", size=7),
Layout(name="position", size=6),
Layout(name="footer", size=3)
)
# Header # 1. Header
header_table = Table(box=box.ROUNDED, expand=True, show_header=False) header = Table(title=f"PING-PONG BOT v{self.version} Dashboard", box=box.ROUNDED, expand=True)
header_table.add_row(f"[bold cyan]PING-PONG BOT v{self.version}[/] | Symbol: [bold white]{self.symbol}[/] | Price: [bold yellow]${self.market_price:,.2f}[/]") header.add_column("Property"); header.add_column("Value")
layout["header"].update(Panel(header_table)) header.add_row("Symbol", self.symbol)
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})")
# Indicators # 2. Indicators
ind_table = Table(box=box.ROUNDED, expand=True) inds = Table(title="INDICATORS", box=box.ROUNDED, expand=True)
ind_table.add_column("Indicator", style="dim"); ind_table.add_column("Value", justify="right"); ind_table.add_column("Last Update", justify="center") inds.add_column("Indicator"); inds.add_column("Value"); inds.add_column("Updated")
ind_table.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'])
ind_table.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'])
ind_table.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'])
layout["indicators"].update(Panel(ind_table, title="[bold yellow]TECHNICAL INDICATORS[/]"))
# Position # 3. Position
pos_table = Table(box=box.ROUNDED, expand=True) pos = Table(title="PORTFOLIO & STATUS", box=box.ROUNDED, expand=True)
pos_table.add_column("Account Balance", justify="center"); pos_table.add_column("Position Size", justify="center"); pos_table.add_column("Entry Price", justify="center"); pos_table.add_column("Unrealized PnL", justify="center") pos.add_column("Property"); pos.add_column("Value")
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_table.add_row(f"${self.wallet_balance:,.2f}", str(self.position['size']), f"${float(self.position['avgPrice']):,.2f}", f"[bold {'green' if pnl>=0 else 'red'}]${pnl:,.2f}") 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}")
else: else:
pos_table.add_row(f"${self.wallet_balance:,.2f}", "0", "-", "-") pos.add_row("Position", "NONE")
layout["position"].update(Panel(pos_table, title="[bold green]PORTFOLIO STATUS[/]"))
# Footer pos.add_row("Status", f"[bold blue]{self.status_msg}[/]")
refresh_str = self.last_refresh_time.strftime("%H:%M:%S") pos.add_row("Last Signal", str(self.last_signal or "None"))
footer_text = f"Status: [bold blue]{self.status_msg}[/]\nLast Signal: [bold yellow]{self.last_signal or 'N/A'}[/] | Last Candle: {self.last_candle_time or 'N/A'} | [dim italic]Heartbeat: {refresh_str}[/]" pos.add_row("Heartbeat", f"[italic]{now}[/]")
layout["footer"].update(Panel(footer_text))
return layout self.console.print("\n")
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...")
await self.db.connect() await self.db.connect()
last_exchange_update = 0 last_exchange_update = 0
with Live(self.generate_dashboard(), refresh_per_second=1) as live:
while True: while True:
try: try:
now = time.time() now_ts = time.time()
self.last_refresh_time = datetime.now()
# 1. Exchange Sync (15s) # 1. Exchange Sync (15s)
if now - last_exchange_update >= 15: if now_ts - last_exchange_update >= 15:
await self.update_exchange_data() await self.update_exchange_data()
last_exchange_update = now last_exchange_update = now_ts
# 2. DB Sync (5s) # 2. DB Sync (5s)
candles = await self.db.get_candles(self.db_symbol, self.db_interval, limit=100) candles = await self.db.get_candles(self.db_symbol, self.db_interval, limit=100)
@ -300,20 +299,21 @@ class PingPongBot:
df = df.astype({'open': float, 'high': float, 'low': float, 'close': float, 'volume': float}) df = df.astype({'open': float, 'high': float, 'low': float, 'close': float, 'volume': float})
df = self.calculate_indicators(df) df = self.calculate_indicators(df)
signal = self.check_signals(df) signal = self.check_signals(df)
if signal: await self.execute_trade(signal) if signal:
logger.info(f"Signal detected: {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 processed: {latest['time']}" self.status_msg = f"New Candle: {latest['time']}"
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. Update Dashboard # 3. Print Dashboard
live.update(self.generate_dashboard()) self.print_dashboard()
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]}"
live.update(self.generate_dashboard())
await asyncio.sleep(5) await asyncio.sleep(5)