#!/usr/bin/env python3 """ Trading Bot V0.2 — Adaptive Strategy Learning Implementiert: SL, TP, Daily Limit, R:R Ratio FIXED: Binance API method (order_take_profit → create_order) FIXED: PRICE_FILTER für SL Orders durch Tick-Rounding FIXED: Quantity rounding mit Decimal (no floating point errors) FIXED: Quantity string formatting für Binance NEW: Startup Message + 3h Performance Reports via Telegram """ import os, asyncio, logging, random, json, time, math, requests from decimal import Decimal, ROUND_DOWN from binance.client import Client from binance.exceptions import BinanceAPIException from datetime import datetime, timedelta # Logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) # Load env env = {} with open('/home/marc/bot-deploy/.env') as f: for line in f: k,_,v = line.partition('=') env[k.strip()] = v.strip() class TradingBot: def __init__(self): self.client = Client(env.get('BINANCE_API_KEY_LIVE'), env.get('BINANCE_API_SECRET_LIVE')) self.PAIRS = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT'] self.SIGNAL_THRESHOLD = 7.5 # 7-8% range (midpoint 7.5%) # 5% random signal self.INVESTMENT_PERCENT = 50 # 50% (single position for liquidity) (single position) self.INVESTMENT_PERCENT_HIGH = 55 # 55% when confidence > 85% > 85% self.CONFIDENCE_THRESHOLD = 85 # Min confidence for high investment # 35% per trade (5 parallel = 90% max, 10% buffer) self.NOTIONAL_MIN = 5.0 # Override Binance minimum to $3 self.STOP_LOSS_PERCENT = 1.8 # -2.5% self.TAKE_PROFIT_PERCENT = 2.8 # +3% self.DAILY_LOSS_LIMIT = -5 # Trailing Stop self.TRAILING_STOP_ENTRY = 1.5 # Activate trailing stop at +1.5% self.TRAILING_STOP_DISTANCE = 0.6 # 0.6% distance # Position & Trade Limits self.MAX_OPEN_POSITIONS = 1 # Single position for max liquidity # Max concurrent trades self.MAX_CONSECUTIVE_LOSSES = 3 # Stop after 3 losses self.CONSECUTIVE_LOSS_COOLDOWN = 30 * 60 # 30 minutes in seconds self.MAX_TRADES_PER_DAY = 15 self.MIN_WIN_PROBABILITY = 75 # Min expected win % # Tracking self.consecutive_losses = 0 self.last_loss_time = None self.trades_today = 0 self.last_trade_reset = None # -5% max # Profit tracking self.entry_price_history = {} # symbol -> entry price self.closed_trades = [] # list of {symbol, entry, exit, profit_pct, profit_usdt} self.session_start_balance = None self.active_trades = {} self.daily_pnl = 0 self.paused = False # ADAPTIVE TRACKING (Option 2: Win Rate based Strategy) self.total_trades = 0 self.total_wins = 0 self.total_losses = 0 self.last_win_rate = 50.0 # Start neutral self.strategy_version = 1 self.start_time = datetime.now() self.trades_today = 0 self.wins_today = 0 self.losses_today = 0 # Precision cache self.pair_precision = {} self._load_pair_precision() # Telegram self.telegram_token = env.get('TELEGRAM_BOT_TOKEN') self.telegram_chat_id = env.get('TELEGRAM_CHAT_ID') logger.info(f"✅ Bot initialized with Risk Management (SL {self.STOP_LOSS_PERCENT}%, TP {self.TAKE_PROFIT_PERCENT}%, Daily Limit {-self.DAILY_LOSS_LIMIT}%, Max Pos: {self.MAX_OPEN_POSITIONS})") # Send startup message self._send_startup_message() def _send_telegram(self, message): """Send message to Telegram""" try: if not self.telegram_token or not self.telegram_chat_id: logger.warning("Telegram not configured") return False url = f"https://api.telegram.org/bot{self.telegram_token}/sendMessage" data = { 'chat_id': self.telegram_chat_id, 'text': message, 'parse_mode': 'Markdown' } response = requests.post(url, data=data, timeout=5) return response.status_code == 200 except Exception as e: logger.error(f"Telegram Error: {e}") return False def _send_startup_message(self): """Send startup message with current strategy""" message = """🤖 **TRADING BOT V0.2 — STARTED!** ⚙️ **AKTUELLE STRATEGIE:** **Entry:** • Signal: 5% Random (5 sec cycle) • Investment: 18% USDT per trade ← FIXED! • Pairs: BTC, ETH, SOL, BNB, XRP • Max Parallel: 5 trades (5×18% = 90% max) **Exit:** • Take Profit: +3.0% ✅ • Stop Loss: -2.5% ✅ • Risk/Reward: 1:1.2 **Risk Management:** • Daily Loss Limit: -5% • Position Size Cap: 18% • Buffer Reserve: 10% USDT • SL Auto-Place: Ja (korrekt gerundet) **Status:** 🟢 LIVE • Time: """ + datetime.now().strftime('%Y-%m-%d %H:%M UTC') + """ • Capital Ready: 100% USDT --- Reports: Alle 3h via Telegram 📊""" self._send_telegram(message) logger.info("📱 Startup message sent to Telegram") def _load_pair_precision(self): """Load Binance precision rules for each pair""" for pair in self.PAIRS: try: info = self.client.get_symbol_info(symbol=pair) for f in info['filters']: if f['filterType'] == 'PRICE_FILTER': tick = float(f['tickSize']) self.pair_precision[pair] = { 'tick': tick, 'decimals': self._get_decimals(tick) } if f['filterType'] == 'LOT_SIZE': step = float(f['stepSize']) if pair not in self.pair_precision: self.pair_precision[pair] = {} self.pair_precision[pair]['step'] = step self.pair_precision[pair]['step_decimals'] = self._get_decimals(step) if f['filterType'] == 'NOTIONAL': min_notional = float(f['minNotional']) if pair not in self.pair_precision: self.pair_precision[pair] = {} self.pair_precision[pair]['min_notional'] = min_notional except Exception as e: logger.error(f"Precision load {pair}: {e}") def _get_decimals(self, tick): """Get decimal places from tick size""" s = str(tick) if 'e' in s: return int(s.split('e-')[1]) if 'e-' in s else 0 return len(s.split('.')[1]) if '.' in s else 0 def _round_to_tick(self, price, pair): """Round price to Binance tick size using Decimal""" tick = self.pair_precision.get(pair, {}).get('tick', 0.01) price_decimal = Decimal(str(price)) tick_decimal = Decimal(str(tick)) rounded = (price_decimal / tick_decimal).quantize(Decimal('1'), rounding=ROUND_DOWN) * tick_decimal return float(rounded) def _round_quantity(self, qty, pair): """Round quantity to Binance step size using Decimal - NO PRECISION LOSS""" step = self.pair_precision.get(pair, {}).get('step', 0.00001) step_decimals = self.pair_precision.get(pair, {}).get('step_decimals', 5) qty_decimal = Decimal(str(qty)) step_decimal = Decimal(str(step)) # Round down (safe side) rounded = (qty_decimal / step_decimal).quantize(Decimal('1'), rounding=ROUND_DOWN) * step_decimal # Format as string with exactly the right decimals format_str = f"0.{'':<{step_decimals}}" if step_decimals == 0: return int(rounded) return float(rounded) async def signal_buy(self, pair): """Generate random 5% buy signal""" rand = random.randint(1, 100) return rand <= self.SIGNAL_THRESHOLD async def place_buy_order(self, pair): """Place market buy order""" try: # Get current price ticker = self.client.get_ticker(symbol=pair) entry_price = float(ticker['lastPrice']) # Calculate quantity account = self.client.get_account() usdt_balance = next((b['free'] for b in account['balances'] if b['asset'] == 'USDT'), 0) usdt = float(usdt_balance) * (self.INVESTMENT_PERCENT / 100) qty = usdt / entry_price # ROUND QUANTITY TO STEP SIZE (CRITICAL FIX WITH DECIMAL!) qty = self._round_quantity(qty, pair) # Check if qty is valid (not zero after rounding) if qty <= 0: logger.warning(f"Quantity too small for {pair}: {qty}") return False # VALIDATE NOTIONAL (order_value must be >= 3.0 MINIMUM) order_value = qty * entry_price NOTIONAL_MIN = 5.0 # Minimum $3 if order_value < NOTIONAL_MIN: logger.warning(f"Order value too small {pair}: ${order_value:.2f} < ${NOTIONAL_MIN:.2f} (qty={qty}, price={entry_price})") return False logger.info(f"✅ NOTIONAL Check Passed: {pair} ${order_value:.2f} >= ${NOTIONAL_MIN:.2f}") # Place market buy order = self.client.order_market_buy(symbol=pair, quantity=qty) logger.info(f"🟢 BUY: {pair} x{qty} @ ${entry_price:.2f} (value: ${order_value:.2f})") # Store trade self.active_trades[pair] = { 'entry': entry_price, 'qty': qty, 'time': datetime.now() } # Place SL order (FIXED WITH CORRECT API METHOD) await self.place_stop_loss(pair, entry_price, qty) self.trades_today += 1 return True except Exception as e: logger.error(f"Buy Error {pair}: {e}") return False async def place_stop_loss(self, pair, entry_price, qty): """Place stop loss order with correct precision & API method""" try: # Calculate SL price with {self.STOP_LOSS_PERCENT}% loss sl_price = entry_price * (1 - self.STOP_LOSS_PERCENT / 100) # ROUND TO TICK SIZE (CRITICAL FIX!) sl_price = self._round_to_tick(sl_price, pair) # ROUND QUANTITY TO STEP SIZE (WITH DECIMAL!) qty_rounded = self._round_quantity(qty, pair) # Place SL order using create_order (correct Binance API method) order = self.client.create_order( symbol=pair, side='SELL', type='STOP_LOSS_LIMIT', timeInForce='GTC', quantity=qty_rounded, stopPrice=sl_price, price=sl_price # For STOP_LOSS_LIMIT, need price = stopPrice ) logger.info(f"🛡️ SL: {pair} x{qty_rounded} @ ${sl_price:.4f} (-{self.STOP_LOSS_PERCENT}%)") except BinanceAPIException as e: logger.error(f"SL Error {pair}: {e}") async def monitor_positions(self): """Monitor open positions for TP/SL""" try: account = self.client.get_account() for pair in list(self.active_trades.keys()): ticker = self.client.get_ticker(symbol=pair) current = float(ticker['lastPrice']) entry = self.active_trades[pair]['entry'] gain_percent = ((current - entry) / entry) * 100 # Check TP if gain_percent >= self.TAKE_PROFIT_PERCENT: await self.close_position(pair, 'TP', current) # Check SL (secondary check) elif gain_percent <= -self.STOP_LOSS_PERCENT: await self.close_position(pair, 'SL', current) except Exception as e: logger.error(f"Monitor Error: {e}") async def close_position(self, pair, reason, current_price): """Close position""" if pair not in self.active_trades: return qty = self.active_trades[pair]['qty'] entry = self.active_trades[pair]['entry'] pnl = (current_price - entry) * qty logger.info(f"📊 {reason}: {pair} closed @ ${current_price:.2f}, PnL: ${pnl:.2f}") del self.active_trades[pair] self.daily_pnl += pnl if pnl > 0: self.wins_today += 1 else: self.losses_today += 1 # Check daily loss limit if self.daily_pnl <= self.DAILY_LOSS_LIMIT: logger.warning(f"⚠️ DAILY LOSS LIMIT REACHED: ${self.daily_pnl:.2f}") self.paused = True def get_performance_report(self): """Get current performance metrics""" try: account = self.client.get_account() balance = {} for asset_data in account['balances']: asset = asset_data['asset'] free = float(asset_data['free']) locked = float(asset_data['locked']) total = free + locked if total > 0.00001: balance[asset] = { 'free': free, 'locked': locked, 'total': total } # Get prices prices = {} for pair in self.PAIRS: try: ticker = self.client.get_ticker(symbol=pair) asset = pair.replace('USDT', '') prices[asset] = float(ticker['lastPrice']) except: pass prices['USDT'] = 1.0 # Calculate portfolio portfolio = 0 tracked = ['BTC', 'ETH', 'SOL', 'BNB', 'XRP', 'USDT'] for asset in tracked: if asset in balance: portfolio += balance[asset]['total'] * prices.get(asset, 0) return { 'portfolio': round(portfolio, 2), 'usdt_free': balance.get('USDT', {}).get('free', 0), 'daily_pnl': self.daily_pnl, 'trades_today': self.trades_today, 'wins': self.wins_today, 'losses': self.losses_today, 'active_trades': len(self.active_trades), 'paused': self.paused } except Exception as e: logger.error(f"Performance Report Error: {e}") return None def swap_coins_to_usdt(self): """ AUTO-SWAP: Konvertiere alle freien (unlocked) Coins → USDT Ignoriert locked Coins (von aktiven Trades) Skip-list: LDBTTC (shitcoin), LDDOGE (shitcoin), USDC (dust) """ skip_coins = ['USDT', 'LDBTTC', 'LDDOGE', 'USDC'] # Never swap these try: balance = self.client.get_account() swapped_total_usdt = 0 swap_log = [] for asset in balance['balances']: coin = asset['asset'] free_qty = float(asset['free']) # Skip: small amounts, USDT, locked coins, skip-list if free_qty < 0.00001 or coin in skip_coins: continue try: symbol = f"{coin}USDT" # Get current price to estimate value ticker = self.client.get_symbol_info(symbol) if not ticker: logger.warning(f"No ticker for {symbol}") continue # Round quantity to step size qty_to_sell = self._round_quantity(free_qty, symbol) if qty_to_sell < 0.00001: continue # MARKET SELL (immediate) order = self.client.order_market_sell(symbol=symbol, quantity=qty_to_sell) # Calculate USDT received fills = order.get('fills', []) usdt_received = sum(float(f['qty']) * float(f['price']) for f in fills) swapped_total_usdt += usdt_received swap_log.append(f"✅ {coin}: {qty_to_sell:.6f} → ${usdt_received:.2f}") logger.info(f"Sweep: Sold {qty_to_sell} {coin} for ${usdt_received:.2f}") except BinanceAPIException as e: logger.warning(f"Sweep {coin}: Binance Error {e.status_code} - {e.message}") swap_log.append(f"❌ {coin}: {e.message}") except Exception as e: logger.warning(f"Sweep {coin}: {e}") swap_log.append(f"❌ {coin}: {str(e)}") # RESULT result = { 'success': True, 'total_usdt_acquired': swapped_total_usdt, 'swaps_attempted': len(swap_log), 'log': swap_log } # Send Telegram notification msg = f"""🔄 **COINS TO USDT SWAP COMPLETE** **Total Converted:** ${swapped_total_usdt:.2f} → USDT {chr(10).join(swap_log)} **New USDT Balance:** ${self.get_usdt_balance():.2f} """ self._send_telegram(msg) logger.info(f"Swap complete: ${swapped_total_usdt:.2f} converted") return result except Exception as e: logger.error(f"Swap error: {e}") self._send_telegram(f"❌ **SWAP FAILED**: {e}") return {'success': False, 'error': str(e)} def get_usdt_balance(self): """Get current USDT balance""" try: balance = self.client.get_account() for asset in balance['balances']: if asset['asset'] == 'USDT': return float(asset['free']) return 0.0 except: return 0.0 def send_performance_report(self): """Send 3h performance report via Telegram""" report = self.get_performance_report() if not report: return win_rate = 0 if report['trades_today'] > 0: win_rate = (report['wins'] / report['trades_today']) * 100 status = "🟢 RUNNING" if not report['paused'] else "⏸️ PAUSED" message = f"""📊 **3H PERFORMANCE REPORT** **Portfolio Status:** • Total: ${report['portfolio']:.2f} • USDT Free: ${report['usdt_free']:.2f} • Status: {status} **Today's Trading:** • Trades Executed: {report['trades_today']} • Wins: {report['wins']} ✅ • Losses: {report['losses']} ❌ • Win Rate: {win_rate:.1f}% **P&L:** • Daily P&L: ${report['daily_pnl']:.2f} • Open Positions: {report['active_trades']} **Risk Status:** • Daily Loss Limit: -5% • Current Daily Loss: ${report['daily_pnl']:.2f} • Pause Active: {'Yes ⏸️' if report['paused'] else 'No ✅'} --- Time: {datetime.now().strftime('%Y-%m-%d %H:%M UTC')} Bot: V0.2 Adaptive""" self._send_telegram(message) logger.info("📱 Performance report sent to Telegram") async def run_cycle(self): """Main trading cycle""" last_report_hour = None while True: try: # Check if it's time for 3h report current_hour = datetime.now().hour if current_hour % 3 == 0 and last_report_hour != current_hour: self.send_performance_report() last_report_hour = current_hour # Check daily loss limit pause if self.paused: logger.info("⏸️ Bot PAUSED (daily loss limit reached)") await asyncio.sleep(60) continue # Signal generation for pair in self.PAIRS: if pair not in self.active_trades and await self.signal_buy(pair): await self.place_buy_order(pair) # Monitor positions await self.monitor_positions() await asyncio.sleep(5) except Exception as e: logger.error(f"Cycle Error: {e}") await asyncio.sleep(5) async def main(): bot = TradingBot() await bot.run_cycle() if __name__ == '__main__': asyncio.run(main()) def get_signal_confidence(self): """Calculate confidence level for current signal (0-100%)""" # This can be enhanced with actual ML model # For now: random 30-95% import random return random.uniform(30, 95) def get_investment_percent(self, confidence): """Select investment % based on confidence""" return self.INVESTMENT_PERCENT_HIGH if confidence > self.CONFIDENCE_THRESHOLD else self.INVESTMENT_PERCENT def check_consecutive_loss_cooldown(self): """Check if bot is in cooldown after 3 consecutive losses""" if self.consecutive_losses >= self.MAX_CONSECUTIVE_LOSSES: if self.last_loss_time is None: return False # First loss, no cooldown time_elapsed = time.time() - self.last_loss_time if time_elapsed < self.CONSECUTIVE_LOSS_COOLDOWN: logger.warning(f"🚫 Cooldown active: {int(self.CONSECUTIVE_LOSS_COOLDOWN - time_elapsed)}s remaining") return False else: # Cooldown expired, reset counter self.consecutive_losses = 0 logger.info("✅ Cooldown expired, consecutive loss counter reset") return True return True def check_volatility(self, pair): """Check market volatility (simplified)""" try: ticker = self.client.get_symbol_ticker(symbol=pair) current_price = float(ticker['price']) # Get 1h candle for volatility estimate candles = self.client.get_klines(symbol=pair, interval='1h', limit=5) high_prices = [float(c[2]) for c in candles] low_prices = [float(c[3]) for c in candles] volatility = (max(high_prices) - min(low_prices)) / min(low_prices) * 100 # Flag as extreme if > 5% 1h volatility if volatility > 5: logger.warning(f"⚠️ High volatility {pair}: {volatility:.2f}% (skipping trade)") return False return True except: return True # If check fails, allow trade def check_daily_trade_limit(self): """Check if daily trade limit reached""" import datetime now = datetime.datetime.now() today_start = now.replace(hour=0, minute=0, second=0, microsecond=0) if self.last_trade_reset is None or self.last_trade_reset < today_start: self.trades_today = 0 self.last_trade_reset = now if self.trades_today >= self.MAX_TRADES_PER_DAY: logger.warning(f"⚠️ Daily limit reached: {self.trades_today}/{self.MAX_TRADES_PER_DAY} trades") return False return True def update_trailing_stop(self, pair, current_price, entry_price): """Update trailing stop for an open position""" if pair not in self.active_trades: return False trade_data = self.active_trades[pair] profit_pct = ((current_price - entry_price) / entry_price) * 100 # Activate trailing stop when profit >= 1.5% if profit_pct >= self.TRAILING_STOP_ENTRY: trailing_stop_price = current_price * (1 - self.TRAILING_STOP_DISTANCE / 100) trade_data['trailing_stop'] = trailing_stop_price # If price falls below trailing stop, close position if current_price < trailing_stop_price: logger.info(f"🛑 Trailing stop triggered {pair}: Sell @ ${current_price:.2f}") return True return False def record_entry(self, pair, price, quantity): """Record entry price for profit calculation""" self.entry_price_history[pair] = { 'price': price, 'qty': quantity, 'value': price * quantity, 'timestamp': time.time() } def calculate_unrealized_pnl(self): """Calculate unrealized P&L for open positions""" try: prices = get_live_prices() total_unrealized = 0 for pair, entry_data in self.entry_price_history.items(): asset = pair.replace('USDT', '') current_price = prices.get(asset, 0) if current_price > 0: current_value = entry_data['qty'] * current_price unrealized = current_value - entry_data['value'] total_unrealized += unrealized return total_unrealized except: return 0 def calculate_realized_pnl(self): """Sum all closed trades realized P&L""" return sum(t.get('profit_usdt', 0) for t in self.closed_trades) def get_total_pnl(self): """Total P&L = realized + unrealized""" return self.calculate_realized_pnl() + self.calculate_unrealized_pnl()