feat: replace rich.live with standard print and add exchange timeouts (v1.3.6)
This commit is contained in:
@ -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})")
|
||||||
|
|
||||||
# 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_ts = time.time()
|
||||||
now = 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)
|
||||||
if candles:
|
if candles:
|
||||||
latest = candles[0]
|
latest = candles[0]
|
||||||
if latest['time'] != self.last_candle_time:
|
if latest['time'] != self.last_candle_time:
|
||||||
df = pd.DataFrame(candles[::-1])
|
df = pd.DataFrame(candles[::-1])
|
||||||
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:
|
||||||
self.last_candle_time = latest['time']
|
logger.info(f"Signal detected: {signal}")
|
||||||
self.last_candle_price = latest['close']
|
await self.execute_trade(signal)
|
||||||
self.status_msg = f"New Candle processed: {latest['time']}"
|
self.last_candle_time = latest['time']
|
||||||
else:
|
self.last_candle_price = latest['close']
|
||||||
self.status_msg = f"No candles found for {self.db_symbol} / {self.db_interval}"
|
self.status_msg = f"New Candle: {latest['time']}"
|
||||||
|
else:
|
||||||
|
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)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
bot = PingPongBot()
|
bot = PingPongBot()
|
||||||
|
|||||||
Reference in New Issue
Block a user