#!/usr/bin/env python3 """Trading Bot v0.4.2 - Win-Rate Optimization (RSI + Support Detection)""" import os, json, time, logging, sqlite3 from datetime import datetime from dotenv import load_dotenv from binance.client import Client logging.basicConfig(level=logging.INFO, format='%(asctime)s | %(message)s') logger = logging.getLogger() load_dotenv() API_KEY = os.getenv('BINANCE_API_KEY_LIVE') API_SECRET = os.getenv('BINANCE_API_SECRET_LIVE') if not API_KEY or not API_SECRET: logger.error("Missing API keys") exit(1) SYMBOLS = ['BTCUSDT', 'ETHUSDT', 'BNBUSDT', 'XRPUSDT', 'SOLUSDT'] TRACKED_COINS = ['BTC', 'ETH', 'BNB', 'XRP', 'SOL'] MIN_TRADE_USDT = 12.00 MAX_POSITION_PCT = 0.07 TAKE_PROFIT_PCT = 0.015 STOP_LOSS_PCT = -0.008 CYCLE_SEC = 60 RSI_PERIOD = 14 RSI_OVERSOLD = 30 RSI_OVERBOUGHT = 70 class TradingBotV042: def __init__(self): self.client = Client(API_KEY, API_SECRET) self.price_history = {sym: [] for sym in SYMBOLS} self.rsi_values = {sym: [] for sym in SYMBOLS} self.active_trades = {} self.portfolio_value = 0 self.max_trade_usdt = 0 # TRADE RECOVERY try: account = self.client.get_account() for b in account['balances']: asset = b['asset'] free = float(b['free']) if asset in TRACKED_COINS and free > 0.0001: symbol = asset + 'USDT' try: price = self.get_current_price(symbol) if price: self.active_trades[symbol] = { 'entry_price': price, 'qty': free, 'entry_time': datetime.now().isoformat() } logger.info(f"[RECOVERED] {symbol} {free} @ {price}") except: pass except Exception as e: logger.warning(f"Recovery failed: {e}") logger.info("[v0.4.2 INIT] RSI + Support-based Entry Signals (55%+ Win-Rate target)") def calculate_rsi(self, prices): """Calculate RSI from price list""" if len(prices) < RSI_PERIOD + 1: return None deltas = [prices[i] - prices[i-1] for i in range(1, len(prices))] gains = [d if d > 0 else 0 for d in deltas[-RSI_PERIOD:]] losses = [abs(d) if d < 0 else 0 for d in deltas[-RSI_PERIOD:]] avg_gain = sum(gains) / RSI_PERIOD avg_loss = sum(losses) / RSI_PERIOD if avg_loss == 0: return 100 if avg_gain > 0 else 0 rs = avg_gain / avg_loss rsi = 100 - (100 / (1 + rs)) return rsi def get_fresh_balance(self): try: account = self.client.get_account() portfolio_value = 0 prices = {'USDT': 1.0} for symbol in SYMBOLS: try: ticker = self.client.get_ticker(symbol=symbol) coin = symbol.replace('USDT', '') prices[coin] = float(ticker['lastPrice']) except: pass for balance in account['balances']: asset = balance['asset'] free = float(balance['free']) if asset in TRACKED_COINS: price = prices.get(asset, 0) portfolio_value += free * price elif asset == 'USDT': portfolio_value += free usdt_available = next((float(b['free']) for b in account['balances'] if b['asset'] == 'USDT'), 0) self.portfolio_value = portfolio_value self.max_trade_usdt = portfolio_value * MAX_POSITION_PCT logger.info(f"[v0.4.2] USDT={usdt_available:.2f} | Portfolio={portfolio_value:.2f}") return usdt_available, portfolio_value except: return 0, 0 def get_current_price(self, symbol): try: ticker = self.client.get_ticker(symbol=symbol) return float(ticker['lastPrice']) except: return None def is_local_minimum(self, symbol): """OLD: Local Minimum (price below last 4 candles)""" if len(self.price_history[symbol]) < 5: return False recent = self.price_history[symbol][-5:] current = recent[-1] is_min = all(current < p for p in recent[:-1]) if is_min: logger.info(f"[SIGNAL-1] LOCAL_MIN: {symbol}") return is_min def is_rsi_oversold(self, symbol): """NEW: RSI oversold (RSI < 30)""" if len(self.price_history[symbol]) < RSI_PERIOD + 2: return False rsi = self.calculate_rsi(self.price_history[symbol]) if not rsi: return False is_oversold = rsi < RSI_OVERSOLD if is_oversold: logger.info(f"[SIGNAL-2] RSI_OVERSOLD: {symbol} RSI={rsi:.1f}") return is_oversold def is_support_bounce(self, symbol): """NEW: Price bouncing from support level (2% rebound)""" if len(self.price_history[symbol]) < 5: return False recent = self.price_history[symbol][-5:] low = min(recent[:-1]) current = recent[-1] # If current is 2%+ above recent low, it's a bounce bounce_pct = ((current - low) / low) * 100 is_bounce = (bounce_pct >= 2.0) if is_bounce: logger.info(f"[SIGNAL-3] SUPPORT_BOUNCE: {symbol} {bounce_pct:.1f}%") return is_bounce def has_buy_signal(self, symbol): """Multiple entry signals for higher Win-Rate""" return ( self.is_local_minimum(symbol) or self.is_rsi_oversold(symbol) or self.is_support_bounce(symbol) ) def calculate_valid_quantity(self, symbol, usdt_amount): try: price = self.get_current_price(symbol) if not price or price <= 0: return 0 info = self.client.get_symbol_info(symbol) if not info: return 0 step_size = None for f in info.get('filters', []): if f['filterType'] == 'LOT_SIZE': step_size = float(f['stepSize']) break if not step_size or step_size <= 0: return 0 qty = usdt_amount / price qty = int(qty / step_size) * step_size if qty * price < 5.0: return 0 return qty except: return 0 def place_buy_order(self, symbol, usdt_amount): try: qty = self.calculate_valid_quantity(symbol, usdt_amount) if qty <= 0: return None price = self.get_current_price(symbol) if not price: return None order = self.client.order_market_buy(symbol=symbol, quantity=qty) self.active_trades[symbol] = { 'entry_price': price, 'qty': qty, 'order_id': order.get('orderId'), 'entry_time': datetime.now().isoformat() } logger.info(f"[BUY-v0.4.2] {symbol} {qty} @ {price}") return order except: return None def check_and_close_positions(self): for symbol, trade in list(self.active_trades.items()): try: current = self.get_current_price(symbol) if not current: continue entry = trade['entry_price'] qty = trade['qty'] pnl_pct = ((current - entry) / entry) * 100 if pnl_pct >= TAKE_PROFIT_PCT * 100: logger.info(f"[SELL-TP] {symbol} +{pnl_pct:.2f}%") try: self.client.order_market_sell(symbol=symbol, quantity=qty) del self.active_trades[symbol] except: pass elif pnl_pct <= STOP_LOSS_PCT * 100: logger.info(f"[SELL-SL] {symbol} {pnl_pct:.2f}%") try: self.client.order_market_sell(symbol=symbol, quantity=qty) del self.active_trades[symbol] except: pass except: pass def save_pnl_to_db(self, portfolio_val, usdt_free): """Save P&L data to database""" try: conn = sqlite3.connect('/home/marc/bot-deploy/pnl_charts.db') baseline = conn.execute('SELECT pv FROM history ORDER BY ts ASC LIMIT 1').fetchone() baseline_pv = baseline[0] if baseline else portfolio_val pu = portfolio_val - baseline_pv pp = (pu / baseline_pv * 100) if baseline_pv > 0 else 0 conn.execute('INSERT INTO history VALUES (?, ?, ?, ?, ?, ?)', (int(datetime.now().timestamp()), portfolio_val, pu, pp, usdt_free, len(self.active_trades))) conn.commit() conn.close() logger.info(f"[DB-LOG] PV={portfolio_val:.2f}, P&L={pp:.2f}%") except Exception as e: logger.warning(f"DB log failed: {e}") def run_cycle(self): logger.info("="*70) usdt_free, portfolio_val = self.get_fresh_balance() if usdt_free < MIN_TRADE_USDT: logger.warning(f"Low capital: {usdt_free}") logger.info("="*70) return self.check_and_close_positions() # Update price history for symbol in SYMBOLS: price = self.get_current_price(symbol) if price: self.price_history[symbol].append(price) if len(self.price_history[symbol]) > 100: self.price_history[symbol].pop(0) # Find BEST signal (any of the 3) best_signal = None for symbol in SYMBOLS: if symbol not in self.active_trades and self.has_buy_signal(symbol): best_signal = symbol break if best_signal and usdt_free >= MIN_TRADE_USDT: trade_amount = min(max(MIN_TRADE_USDT, self.max_trade_usdt), usdt_free * 0.5) self.place_buy_order(best_signal, trade_amount) # Save trades try: temp = '/home/marc/bot-deploy/active_trades.json.tmp' with open(temp, 'w') as f: json.dump({ 'active_trades': self.active_trades, 'count': len(self.active_trades), 'portfolio_value': round(portfolio_val, 2), 'max_trade_usdt': round(self.max_trade_usdt, 2), 'timestamp': datetime.now().isoformat(), 'version': 'v0.4.2' }, f) os.replace(temp, '/home/marc/bot-deploy/active_trades.json') except: pass # Save P&L self.save_pnl_to_db(portfolio_val, usdt_free) logger.info(f"[CYCLE-END] Trades={len(self.active_trades)} | Portfolio={portfolio_val:.2f}") logger.info("="*70) if __name__ == '__main__': import sys bot = TradingBotV042() if len(sys.argv) > 1 and sys.argv[1] == '--once': bot.run_cycle() else: logger.info("[v0.4.2 START] Bot running (RSI + Support Signals)...") while True: try: bot.run_cycle() except Exception as e: logger.error(f"Error: {e}") time.sleep(CYCLE_SEC)