revert: remove all blinking related Live UI changes (v1.4.1)
This commit is contained in:
@ -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})")
|
|
||||||
|
|
||||||
# 2. Indicators Table
|
pos_table = Table(title="POSITION", box=box.ROUNDED, expand=True)
|
||||||
inds = Table(box=box.SIMPLE, expand=True)
|
pos_table.add_column("Wallet"); pos_table.add_column("Size"); pos_table.add_column("Entry"); pos_table.add_column("PnL")
|
||||||
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:
|
||||||
|
try:
|
||||||
|
now = time.time()
|
||||||
|
# 1. Exchange Sync (15s)
|
||||||
|
if now - last_exchange_update >= 15:
|
||||||
|
await self.update_exchange_data()
|
||||||
|
last_exchange_update = now
|
||||||
|
|
||||||
with Live(self.get_dashboard(), console=self.console, refresh_per_second=1, screen=False) as live:
|
# 2. DB Sync (5s)
|
||||||
while True:
|
candles = await self.db.get_candles(self.db_symbol, self.db_interval, limit=100)
|
||||||
try:
|
if candles:
|
||||||
now_ts = time.time()
|
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}"
|
||||||
|
|
||||||
# 1. Exchange Sync (15s)
|
self.render_dashboard()
|
||||||
if now_ts - last_exchange_update >= 15:
|
except Exception as e:
|
||||||
await asyncio.wait_for(self.update_exchange_data(), timeout=10)
|
logger.error(f"Loop error: {e}")
|
||||||
last_exchange_update = now_ts
|
self.status_msg = f"Error: {str(e)[:40]}"
|
||||||
|
|
||||||
# 2. DB Sync (5s)
|
await asyncio.sleep(5)
|
||||||
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)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
bot = PingPongBot()
|
bot = PingPongBot()
|
||||||
|
|||||||
Reference in New Issue
Block a user