docs: add DOCKER_GUIDE.md and fix .env parsing; chore: update docker and script configurations

This commit is contained in:
BTC Bot
2026-03-05 08:20:18 +01:00
parent e41afcf005
commit 30aeda0901
25 changed files with 1806 additions and 0 deletions

View File

@ -0,0 +1,408 @@
import os
import time
import yaml
import hmac
import hashlib
import json
import logging
import asyncio
import pandas as pd
import numpy as np
from datetime import datetime, timezone
from dotenv import load_dotenv
from rich.console import Console
from rich.table import Table
from rich.live import Live
from rich.panel import Panel
from rich.layout import Layout
from rich import box
# Try to import pybit, if not available, we'll suggest installing it
try:
from pybit.unified_trading import HTTP
except ImportError:
print("Error: 'pybit' library not found. Please install it with: pip install pybit")
exit(1)
# Load environment variables
load_dotenv()
# Setup Logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
filename='logs/ping_pong_bot.log'
)
logger = logging.getLogger("PingPongBot")
console = Console()
class PingPongBot:
def __init__(self, config_path="config/ping_pong_config.yaml"):
with open(config_path, 'r') as f:
self.config = yaml.safe_load(f)
self.api_key = os.getenv("API_KEY")
self.api_secret = os.getenv("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")
self.session = HTTP(
testnet=False,
api_key=self.api_key,
api_secret=self.api_secret,
)
self.symbol = self.config['symbol']
self.interval = self.config['interval']
self.direction = self.config['direction'].lower()
# State
self.last_candle_time = None
self.current_indicators = {}
self.position = None
self.wallet_balance = 0
self.status_msg = "Initializing..."
self.last_signal = None
self.start_time = datetime.now()
# Grid parameters from config
self.tp_pct = self.config['take_profit_pct'] / 100.0
self.partial_exit_pct = self.config['partial_exit_pct']
self.min_val_usd = self.config['min_position_value_usd']
self.pos_size_margin = self.config['pos_size_margin']
self.leverage = self.config['exchange_leverage']
self.max_eff_lev = self.config['max_effective_leverage']
def rma(self, series, length):
"""Rolling Moving Average (Wilder's Smoothing) - matches Pine Script ta.rma"""
alpha = 1 / length
return series.ewm(alpha=alpha, adjust=False).mean()
def calculate_indicators(self, df):
"""Calculate RSI and Hurst Bands matching the JS/Dashboard implementation"""
# 1. RSI
rsi_cfg = self.config['rsi']
delta = df['close'].diff()
gain = (delta.where(delta > 0, 0))
loss = (-delta.where(delta < 0, 0))
avg_gain = self.rma(gain, rsi_cfg['period'])
avg_loss = self.rma(loss, rsi_cfg['period'])
rs = avg_gain / avg_loss
df['rsi'] = 100 - (100 / (1 + rs))
# 2. Hurst Bands
hurst_cfg = self.config['hurst']
mcl_t = hurst_cfg['period']
mcm = hurst_cfg['multiplier']
mcl = mcl_t / 2
mcl_2 = int(round(mcl / 2))
# True Range
df['h_l'] = df['high'] - df['low']
df['h_pc'] = abs(df['high'] - df['close'].shift(1))
df['l_pc'] = abs(df['low'] - df['close'].shift(1))
df['tr'] = df[['h_l', 'h_pc', 'l_pc']].max(axis=1)
# RMA of Close and ATR
df['ma_mcl'] = self.rma(df['close'], mcl)
df['atr_mcl'] = self.rma(df['tr'], mcl)
# Historical Offset
df['center'] = df['ma_mcl'].shift(mcl_2)
# Fill first values where shift produces NaN with the MA itself (as done in JS: historical_ma || src)
df['center'] = df['center'].fillna(df['ma_mcl'])
mcm_off = mcm * df['atr_mcl']
df['hurst_upper'] = df['center'] + mcm_off
df['hurst_lower'] = df['center'] - mcm_off
return df
async def fetch_data(self):
"""Fetch latest Klines from Bybit V5"""
try:
# We fetch 200 candles to ensure indicators stabilize
response = self.session.get_kline(
category="linear",
symbol=self.symbol,
interval=self.interval,
limit=200
)
if response['retCode'] != 0:
self.status_msg = f"API Error: {response['retMsg']}"
return None
klines = response['result']['list']
# Bybit returns newest first, we need oldest first
df = pd.DataFrame(klines, columns=['start_time', 'open', 'high', 'low', 'close', 'volume', 'turnover'])
df = df.astype(float)
df = df.iloc[::-1].reset_index(drop=True)
return self.calculate_indicators(df)
except Exception as e:
logger.error(f"Error fetching data: {e}")
self.status_msg = f"Fetch Error: {str(e)}"
return None
async def update_account_info(self):
"""Update position and balance information"""
try:
# Get Position
pos_response = self.session.get_positions(
category="linear",
symbol=self.symbol
)
if pos_response['retCode'] == 0:
positions = pos_response['result']['list']
# Filter by side or just take the one with size > 0
active_pos = [p for p in positions if float(p['size']) > 0]
if active_pos:
self.position = active_pos[0]
else:
self.position = None
# Get Balance
wallet_response = self.session.get_wallet_balance(
category="linear",
coin="USDT"
)
if wallet_response['retCode'] == 0:
self.wallet_balance = float(wallet_response['result']['list'][0]['coin'][0]['walletBalance'])
except Exception as e:
logger.error(f"Error updating account info: {e}")
def check_signals(self, df):
"""Determine if we should Open or Close based on indicators"""
if len(df) < 2:
return None
last = df.iloc[-1]
prev = df.iloc[-2]
rsi_cfg = self.config['rsi']
hurst_cfg = self.config['hurst']
open_signal = False
close_signal = False
# 1. RSI Signals
rsi_buy = prev['rsi'] < rsi_cfg['oversold'] and last['rsi'] >= rsi_cfg['oversold']
rsi_sell = prev['rsi'] > rsi_cfg['overbought'] and last['rsi'] <= rsi_cfg['overbought']
# 2. Hurst Signals
hurst_buy = prev['close'] > prev['hurst_lower'] and last['close'] <= last['hurst_lower']
hurst_sell = prev['close'] > prev['hurst_upper'] and last['close'] <= last['hurst_upper']
# Logic for LONG
if self.direction == 'long':
if (rsi_cfg['enabled_for_open'] and rsi_buy) or (hurst_cfg['enabled_for_open'] and hurst_buy):
open_signal = True
if (rsi_cfg['enabled_for_close'] and rsi_sell) or (hurst_cfg['enabled_for_close'] and hurst_sell):
close_signal = True
# Logic for SHORT
else:
if (rsi_cfg['enabled_for_open'] and rsi_sell) or (hurst_cfg['enabled_for_open'] and hurst_sell):
open_signal = True
if (rsi_cfg['enabled_for_close'] and rsi_buy) or (hurst_cfg['enabled_for_close'] and hurst_buy):
close_signal = True
return "open" if open_signal else ("close" if close_signal else None)
async def execute_trade_logic(self, df, signal):
"""Apply the Ping-Pong strategy logic (Accumulation + TP)"""
last_price = float(df.iloc[-1]['close'])
# 1. Check Take Profit (TP)
if self.position:
avg_price = float(self.position['avgPrice'])
current_qty = float(self.position['size'])
is_tp = False
if self.direction == 'long':
if last_price >= avg_price * (1 + self.tp_pct):
is_tp = True
else:
if last_price <= avg_price * (1 - self.tp_pct):
is_tp = True
if is_tp:
qty_to_close = current_qty * self.partial_exit_pct
remaining_qty = current_qty - qty_to_close
# Min size check
if (remaining_qty * last_price) < self.min_val_usd:
qty_to_close = current_qty
self.status_msg = "TP: Closing Full Position (Min Size reached)"
else:
self.status_msg = f"TP: Closing Partial {self.partial_exit_pct*100}%"
self.place_order(qty_to_close, last_price, is_close=True)
return
# 2. Check Close Signal
if signal == "close" and self.position:
current_qty = float(self.position['size'])
qty_to_close = current_qty * self.partial_exit_pct
if (current_qty - qty_to_close) * last_price < self.min_val_usd:
qty_to_close = current_qty
self.status_msg = "Signal: Closing Position (Partial/Full)"
self.place_order(qty_to_close, last_price, is_close=True)
return
# 3. Check Open/Accumulate Signal
if signal == "open":
# Check Max Effective Leverage
current_qty = float(self.position['size']) if self.position else 0
current_notional = current_qty * last_price
entry_notional = self.pos_size_margin * self.leverage
projected_notional = current_notional + entry_notional
effective_leverage = projected_notional / max(self.wallet_balance, 1.0)
if effective_leverage <= self.max_eff_lev:
qty_to_open = entry_notional / last_price
# Round qty based on symbol precision (simplified)
qty_to_open = round(qty_to_open, 3)
self.status_msg = f"Signal: Opening/Accumulating {qty_to_open} units"
self.place_order(qty_to_open, last_price, is_close=False)
else:
self.status_msg = f"Signal Ignored: Max Leverage {effective_leverage:.2f} > {self.max_eff_lev}"
def place_order(self, qty, price, is_close=False):
"""Send order to Bybit V5"""
side = ""
if self.direction == "long":
side = "Sell" if is_close else "Buy"
else:
side = "Buy" if is_close else "Sell"
try:
response = self.session.place_order(
category="linear",
symbol=self.symbol,
side=side,
orderType="Market",
qty=str(qty),
timeInForce="GTC",
reduceOnly=is_close
)
if response['retCode'] == 0:
logger.info(f"Order Placed: {side} {qty} {self.symbol}")
self.last_signal = f"{side} {qty} @ Market"
else:
logger.error(f"Order Failed: {response['retMsg']}")
self.status_msg = f"Order Error: {response['retMsg']}"
except Exception as e:
logger.error(f"Execution Error: {e}")
self.status_msg = f"Exec Error: {str(e)}"
def create_dashboard(self, df):
"""Create a Rich layout for status display"""
layout = Layout()
layout.split_column(
Layout(name="header", size=3),
Layout(name="main", ratio=1),
Layout(name="footer", size=3)
)
# Header
header_table = Table.grid(expand=True)
header_table.add_column(justify="left", ratio=1)
header_table.add_column(justify="right", ratio=1)
runtime = str(datetime.now() - self.start_time).split('.')[0]
header_table.add_row(
f"[bold cyan]Ping-Pong Bot v1.0[/bold cyan] | Symbol: [yellow]{self.symbol}[/yellow] | TF: [yellow]{self.interval}m[/yellow]",
f"Runtime: [green]{runtime}[/green] | Time: {datetime.now().strftime('%H:%M:%S')}"
)
layout["header"].update(Panel(header_table, style="white on blue"))
# Main Content
main_table = Table(box=box.SIMPLE, expand=True)
main_table.add_column("Category", style="cyan")
main_table.add_column("Value", style="white")
# Indicators
last = df.iloc[-1]
rsi_val = f"{last['rsi']:.2f}"
rsi_status = "[green]Oversold[/green]" if last['rsi'] < self.config['rsi']['oversold'] else ("[red]Overbought[/red]" if last['rsi'] > self.config['rsi']['overbought'] else "Neutral")
main_table.add_row("Price", f"{last['close']:.2f}")
main_table.add_row("RSI", f"{rsi_val} ({rsi_status})")
main_table.add_row("Hurst Upper", f"{last['hurst_upper']:.2f}")
main_table.add_row("Hurst Lower", f"{last['hurst_lower']:.2f}")
main_table.add_section()
# Position Info
if self.position:
size = self.position['size']
avg_p = self.position['avgPrice']
upnl = float(self.position['unrealisedPnl'])
upnl_style = "green" if upnl >= 0 else "red"
main_table.add_row("Position Size", f"{size}")
main_table.add_row("Avg Entry", f"{avg_p}")
main_table.add_row("Unrealized PnL", f"[{upnl_style}]{upnl:.2f} USDT[/{upnl_style}]")
else:
main_table.add_row("Position", "None")
main_table.add_row("Wallet Balance", f"{self.wallet_balance:.2f} USDT")
layout["main"].update(Panel(main_table, title="Current Status", border_style="cyan"))
# Footer
footer_text = f"Status: [bold white]{self.status_msg}[/bold white]"
if self.last_signal:
footer_text += f" | Last Action: [yellow]{self.last_signal}[/yellow]"
layout["footer"].update(Panel(footer_text, border_style="yellow"))
return layout
async def run(self):
"""Main loop"""
with Live(console=console, refresh_per_second=1) as live:
while True:
# 1. Update Account
await self.update_account_info()
# 2. Fetch Data & Calculate Indicators
df = await self.fetch_data()
if df is not None:
# 3. Check for New Candle (for signal processing)
current_time = df.iloc[-1]['start_time']
# 4. Strategy Logic
signal = self.check_signals(df)
await self.execute_trade_logic(df, signal)
# 5. Update UI
live.update(self.create_dashboard(df))
await asyncio.sleep(self.config.get('loop_interval_seconds', 5))
if __name__ == "__main__":
try:
bot = PingPongBot()
asyncio.run(bot.run())
except KeyboardInterrupt:
console.print("\n[bold red]Bot Stopped by User[/bold red]")
except Exception as e:
console.print(f"\n[bold red]Critical Error: {e}[/bold red]")
logger.exception("Critical Error in main loop")