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,86 +234,88 @@ 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})")
# 2. Indicators
inds = Table(title="INDICATORS", box=box.ROUNDED, expand=True)
inds.add_column("Indicator"); inds.add_column("Value"); inds.add_column("Updated")
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'])
# Indicators # 3. Position
ind_table = Table(box=box.ROUNDED, expand=True) pos = Table(title="PORTFOLIO & STATUS", 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") pos.add_column("Property"); pos.add_column("Value")
ind_table.add_row("Hurst Upper", f"{self.current_indicators['hurst_upper']['value']:.2f}", self.current_indicators['hurst_upper']['timestamp']) pos.add_row("Wallet Balance", f"${self.wallet_balance:,.2f}")
ind_table.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'])
layout["indicators"].update(Panel(ind_table, title="[bold yellow]TECHNICAL INDICATORS[/]"))
# Position
pos_table = Table(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")
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
refresh_str = self.last_refresh_time.strftime("%H:%M:%S")
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}[/]"
layout["footer"].update(Panel(footer_text))
return layout pos.add_row("Status", f"[bold blue]{self.status_msg}[/]")
pos.add_row("Last Signal", str(self.last_signal or "None"))
pos.add_row("Heartbeat", f"[italic]{now}[/]")
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_ts = time.time()
now = time.time()
self.last_refresh_time = datetime.now()
# 1. Exchange Sync (15s)
if now - last_exchange_update >= 15:
await self.update_exchange_data()
last_exchange_update = now
# 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 processed: {latest['time']}"
else:
self.status_msg = f"No candles found for {self.db_symbol} / {self.db_interval}"
# 3. Update Dashboard
live.update(self.generate_dashboard())
except Exception as e:
logger.exception(f"Loop Error: {e}")
self.status_msg = f"Error: {str(e)[:50]}"
live.update(self.generate_dashboard())
await asyncio.sleep(5) # 1. Exchange Sync (15s)
if now_ts - last_exchange_update >= 15:
await self.update_exchange_data()
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 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']}"
else:
self.status_msg = f"No candles found for {self.db_symbol}/{self.db_interval}"
# 3. Print Dashboard
self.print_dashboard()
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()