import asyncio, logging, joblib, time, json, aiohttp, os from datetime import datetime, timedelta from src.config import get_config from src.bot.binance_client import BinanceClientWrapper from src.integrations.telegram_notifier import TelegramNotifier from src.integrations.obsidian_logger import ObsidianLogger from src.strategies.ml_strategy import MLStrategy logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) # DASHBOARD CLIENT - MOCK (sends REAL Binance data only via HTTP) class DashboardClient: """Send ONLY REAL, VERIFIED trades to dashboard""" async def record_buy(self, pair, qty, price): """Record BUY - VERIFY on Binance first!""" try: async with aiohttp.ClientSession() as session: async with session.post('http://localhost:7000/api/trade/buy', json={ 'pair': pair, 'qty': qty, 'price': price, 'entry_time': datetime.now().isoformat() }) as resp: if resp.status == 200: logger.info(f'✅ Dashboard recorded BUY: {pair}') else: logger.warning(f'❌ Dashboard BUY record failed: {resp.status}') except Exception as e: logger.warning(f'Dashboard BUY error: {e}') async def record_sell(self, pair, qty, price, profit_usd, profit_pct, hold_time_min): """Record SELL - ONLY IF REAL!""" try: async with aiohttp.ClientSession() as session: async with session.post('http://localhost:7000/api/trade/sell', json={ 'pair': pair, 'qty': qty, 'price': price, 'profit_usd': profit_usd, 'profit_pct': profit_pct, 'hold_time_min': hold_time_min, 'exit_time': datetime.now().isoformat() }) as resp: if resp.status == 200: logger.info(f'✅ Dashboard recorded SELL: {pair} profit=${profit_usd:.2f}') else: logger.warning(f'❌ Dashboard SELL record failed: {resp.status}') except Exception as e: logger.warning(f'Dashboard SELL error: {e}') async def update_state(self, balance, daily_pnl, portfolio_value_usd, total_pnl=0.0, trades_today=0, wins_today=0, losses_today=0): """Update state - REAL DATA ONLY""" try: portfolio_value_chf = portfolio_value_usd * 0.84 async with aiohttp.ClientSession() as session: async with session.post('http://localhost:7000/api/update', json={ 'balance': balance, 'daily_pnl': daily_pnl, 'total_pnl': total_pnl, 'trades_today': trades_today, 'wins_today': wins_today, 'losses_today': losses_today, 'portfolio_value_usd': portfolio_value_usd, 'portfolio_value_chf': portfolio_value_chf, 'last_update': datetime.now().isoformat() }) as resp: if resp.status != 200: logger.warning(f'Dashboard state update failed: {resp.status}') except Exception as e: logger.debug(f'Dashboard update error: {e}') async def clear_state(self): """Clear dashboard - START FRESH""" try: async with aiohttp.ClientSession() as session: async with session.post('http://localhost:7000/api/clear') as resp: logger.info(f'Dashboard cleared: {resp.status}') except: pass class MLTradingBot: def __init__(self, config, binance, telegram, obsidian, model, scaler): self.config = config self.binance = binance self.telegram = telegram self.obsidian = obsidian self.model = model self.scaler = scaler self.dashboard = DashboardClient() # ADD THIS self.strategy = MLStrategy(trading_pair=config.trading_pair) self.last_report_time = time.time() self.report_interval = 10800 self.trades_today = 0 self.wins_today = 0 self.losses_today = 0 self.daily_pnl = 0.0 self.report_count = 0 self.last_swap_time = time.time() # Metriken für echte Win-Rate self.total_trades = 0 self.total_pnl = 0.0 self.max_drawdown = 0.0 self.min_daily_pnl = 0.0 self.starting_capital = 100.0 self.daily_loss_limit_reached = False # Symbol constraints cache self.symbol_info = {} self.pairs = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT'] # Track open positions self.open_positions = {} # CLEAN START - reset on bot restart logger.info('🗑️ RESET: Cleared all stored positions (dashboard will show REAL Binance state only)') # Error tracking & resilience self.last_error_time = 0 self.error_threshold = 5 self.error_count = 0 self.error_cooldown_until = 0 # Daily reset tracking self.last_daily_reset = datetime.utcnow().date() async def load_exchange_info(self): """Load LOT_SIZE constraints for all trading pairs""" try: logger.info('📥 Loading Binance symbol constraints...') for pair in self.pairs: try: info = await self.binance.get_exchange_info(pair) if info: self.symbol_info[pair] = info logger.debug(f'✅ {pair}: minNotional=${info.get("minNotional", 0)}, stepSize={info.get("stepSize", 0)}') except Exception as e: logger.warning(f'Failed to load {pair}: {e}') logger.info(f'✅ Loaded constraints for {len(self.symbol_info)} pairs') except Exception as e: logger.error(f'Failed to load exchange info: {e}') def calculate_valid_quantity(self, pair: str, usdt_amount: float, price: float) -> float: """Calculate order quantity respecting Binance LOT_SIZE constraints.""" try: if pair not in self.symbol_info: logger.warning(f'⚠️ No info for {pair}') return 0 if price is None or price <= 0: logger.warning(f'⚠️ Invalid price for {pair}: {price}') return 0 info = self.symbol_info[pair] step_size = float(info.get('stepSize', 1e-8)) min_qty = float(info.get('minQty', 0)) max_qty = float(info.get('maxQty', 1e10)) min_notional = float(info.get('minNotional', 10)) qty = usdt_amount / price notional = qty * price # CHECK MINNOTIONAL BEFORE ROUNDING if notional < min_notional: logger.debug(f'❌ {pair}: notional ${notional:.2f} < min ${min_notional:.2f} (requested ${usdt_amount:.2f})') return 0 if step_size > 0: qty = round(qty / step_size) * step_size # RECHECK NOTIONAL AFTER ROUNDING (rounding might make qty too small!) notional_after = qty * price if notional_after < min_notional: logger.debug(f'❌ {pair}: after rounding notional ${notional_after:.2f} < min ${min_notional:.2f}') return 0 if qty < min_qty: logger.debug(f'❌ {pair}: qty {qty:.8f} < min {min_qty:.8f}') return 0 if qty > max_qty: qty = max_qty logger.debug(f'✅ {pair}: qty={qty:.8f} (notional=${notional:.2f})') return qty except Exception as e: logger.error(f'Quantity calculation error: {e}') return 0 def format_quantity(self, qty: float, step_size: float) -> str: """Format quantity to proper decimal places - STRICTLY NO scientific notation""" try: if step_size is None or step_size <= 0: # Use Decimal for strict formatting from decimal import Decimal, ROUND_DOWN d = Decimal(str(qty)) return str(d.quantize(Decimal('0.00000001'), rounding=ROUND_DOWN)) if step_size >= 1: return str(int(qty)) # Force no scientific notation using format string from decimal import Decimal, ROUND_DOWN # Determine decimal places from step_size step_str = str(step_size) if 'e' in step_str.lower(): # Scientific notation in step_size decimal_places = 8 elif '.' in step_str: decimal_places = len(step_str.split('.')[-1]) else: decimal_places = 0 decimal_places = max(decimal_places, 1) decimal_places = min(decimal_places, 20) # Round down to step_size multiplier = 10 ** decimal_places rounded_qty = int(qty * multiplier) / multiplier # Format without scientific notation formatted = f'{rounded_qty:.{decimal_places}f}' # Validate: no 'e' in result if 'e' in formatted.lower(): logger.error(f'SCIENTIFIC NOTATION DETECTED: {qty} → {formatted}') return f'{rounded_qty:.8f}' logger.debug(f'Formatted: {qty} → {formatted} (step={step_size})') return formatted except Exception as e: logger.error(f'Format error: {e}') # Fallback: use Decimal from decimal import Decimal d = Decimal(str(qty)) return str(d) def increment_error_count(self) -> bool: """Track error frequency — return True if limit reached""" self.error_count += 1 self.last_error_time = time.time() if self.error_count >= self.error_threshold: self.error_cooldown_until = time.time() + 300 logger.warning(f'🛑 Error threshold ({self.error_count}) reached! Pausing 5 minutes.') return True return False def reset_error_count(self): """Reset error counter if no errors in 60 seconds""" if time.time() - self.last_error_time > 60: if self.error_count > 0: logger.info(f'✅ Error counter reset (was {self.error_count})') self.error_count = 0 async def liquidate_btc_to_usdt_on_startup(self): """ONE-TIME: Sell all BTC to USDT if USDT is too small""" pass # Disabled - use force_liquidate_all instead async def force_liquidate_all(self): """MANUAL LIQUIDATION: Force sell ALL holdings to USDT (Marc can trigger on demand)""" logger.warning('🔥 FORCE LIQUIDATION STARTED') results = {} try: balance = await self.binance.get_balance() # Liquidate BTC (round down to valid LOT_SIZE) btc_free = float(balance.get('BTC', {}).get('free', 0)) if btc_free > 0.00001: try: btc_qty = int(btc_free * 100000) / 100000 btc_qty = max(btc_qty, 0.00001) logger.info(f'📤 Selling {btc_qty:.8f} BTC') result = await self.binance.place_order('BTCUSDT', 'SELL', f'{btc_qty:.8f}', 'MARKET') if result: results['BTC'] = {'status': result.get('status'), 'qty': btc_qty} logger.info(f'✅ BTC SOLD: {result.get("status")}') except Exception as e: logger.error(f'BTC SELL failed: {e}') results['BTC'] = {'error': str(e)[:50]} # Liquidate other holdings for asset in ['ETH', 'SOL', 'BNB', 'XRP']: qty = float(balance.get(asset, {}).get('free', 0)) if qty > 0.001: try: pair = f'{asset}USDT' logger.info(f'📤 Selling {qty:.8f} {asset}') result = await self.binance.place_order(pair, 'SELL', f'{qty:.8f}', 'MARKET') if result: results[asset] = {'status': result.get('status'), 'qty': qty} logger.info(f'✅ {asset} SOLD: {result.get("status")}') except Exception as e: logger.error(f'{asset} SELL failed: {e}') results[asset] = {'error': str(e)[:50]} await asyncio.sleep(2) new_balance = await self.binance.get_balance() new_usdt = float(new_balance.get('USDT', {}).get('free', 0)) logger.warning(f'🔥 LIQUIDATION DONE! New USDT: ${new_usdt:.2f}') return {'status': 'success', 'results': results, 'new_usdt': new_usdt} except Exception as e: logger.error(f'Liquidation error: {e}') return {'status': 'error', 'message': str(e)[:100]} async def auto_swap_holdings_to_usdt(self): """ONE-TIME: Sell all BTC to USDT if USDT is too small""" try: balance = await self.binance.get_balance() btc_free = float(balance.get('BTC', {}).get('free', 0)) usdt_free = float(balance.get('USDT', {}).get('free', 0)) btc_price = 62500 # Approximate current price # If BTC > 0.001 AND USDT < $10, LIQUIDATE BTC if btc_free > 0.0001 and usdt_free < 10: logger.warning(f'🔄 STARTUP LIQUIDATION: Selling {btc_free:.8f} BTC (~${btc_free * btc_price:.2f}) to USDT...') try: # Use calculate_valid_quantity to respect LOT_SIZE valid_qty = self.calculate_valid_quantity('BTCUSDT', btc_free * btc_price, btc_price) if valid_qty <= 0: logger.warning(f'⚠️ BTC qty too small after LOT_SIZE check: {valid_qty}') return # IMPORTANT: Don't sell MORE than we actually have! valid_qty = min(valid_qty, btc_free) logger.info(f'📤 Placing SELL: {valid_qty:.8f} BTC (actual balance: {btc_free:.8f})') result = await self.binance.place_order( symbol='BTCUSDT', side='SELL', quantity=f'{valid_qty:.8f}', order_type='MARKET' ) if result and result.get('status') in ['FILLED', 'NEW', 'PARTIALLY_FILLED']: logger.info(f'✅ BTC LIQUIDATED! Order ID: {result.get("orderId")}, Status: {result.get("status")}') # Wait for balance to update await asyncio.sleep(3) new_balance = await self.binance.get_balance() new_usdt = float(new_balance.get('USDT', {}).get('free', 0)) new_btc = float(new_balance.get('BTC', {}).get('free', 0)) logger.info(f'💰 After liquidation: USDT=${new_usdt:.2f}, BTC={new_btc:.8f}') except Exception as e: logger.error(f'❌ Liquidation order failed: {type(e).__name__}: {str(e)[:100]}') except Exception as e: logger.error(f'Liquidation check failed: {e}') async def auto_swap_periodically(self): """Periodically swap small holdings back to USDT for liquidity""" try: if time.time() - self.last_swap_time < 300: return if time.time() < self.error_cooldown_until: logger.debug('⏸️ Error cooldown active — skipping swap') return balance = await self.binance.get_balance() if not balance: logger.warning('No balance data for swap') return swapped_any = False for asset in ['BTC', 'ETH', 'SOL', 'BNB', 'XRP']: try: qty = float(balance.get(asset, {}).get('free', 0)) if qty <= 0.00001: continue pair = f'{asset}USDT' # FIX: Robust price fetching with error handling try: price = await self.binance.get_ticker_price(pair) if price is None or price <= 0: logger.warning(f'⚠️ Invalid price for {pair}: {price} — skipping swap') continue except Exception as e: logger.warning(f'Failed to get price for {pair}: {e}') continue notional = qty * price # Only swap if in safe range if notional < 5 or notional > 15: logger.debug(f'⏸️ {pair} notional ${notional:.2f} outside swap range [5-15]') continue logger.info(f'🔄 ATTEMPTING SWAP: {qty:.8f} {asset} (${notional:.2f}) → USDT @ ${price:.2f}') try: if pair not in self.symbol_info: await self.load_exchange_info() if pair not in self.symbol_info: logger.warning(f'No symbol info for {pair} — skipping') continue step_size = self.symbol_info[pair].get('stepSize', 1e-8) qty_str = self.format_quantity(qty, step_size) logger.info(f'📤 Placing SWAP SELL: {qty_str} {pair} @ ${price:.2f}') result = await self.binance.place_order( symbol=pair, side='SELL', quantity=qty_str, # Pass STRING order_type='MARKET' ) if result: # SWAP Alert disabled — user only wants profit notifications pass swapped_any = True self.error_count = 0 # Reset errors on success logger.info(f'✅ SWAP EXECUTED!') else: logger.warning(f'SWAP order returned no result for {pair}') except Exception as e: logger.warning(f'Swap order failed for {asset}: {e}') self.increment_error_count() continue except Exception as e: logger.warning(f'Swap check error for {asset}: {e}') continue if swapped_any: self.last_swap_time = time.time() except Exception as e: logger.error(f'Auto-swap error: {e}') async def check_take_profit(self): """Check all positions for exits""" try: balance = await self.binance.get_balance() if not balance: return positions_to_close = [] for pair in self.pairs: try: asset = pair.replace('USDT', '') current_qty = float(balance.get(asset, {}).get('free', 0)) if current_qty <= 0.00001: continue # FIX: Robust price fetching try: current_price = await self.binance.get_ticker_price(pair) if current_price is None or current_price <= 0: logger.debug(f'⚠️ Invalid price for {pair}: {current_price}') continue except Exception as e: logger.warning(f'Failed to get price for {pair}: {e}') continue if pair not in self.open_positions: logger.debug(f'⏭️ {pair} not in open_positions (keys: {list(self.open_positions.keys())})') continue pos = self.open_positions[pair] buy_price = pos['buy_price'] buy_qty = pos['qty'] buy_time = datetime.fromisoformat(pos['buy_time']) profit_pct = ((current_price - buy_price) / buy_price) * 100 hold_time_minutes = (datetime.now() - buy_time).total_seconds() / 60 if profit_pct > pos.get('peak_profit', 0): pos['peak_profit'] = profit_pct exit_reason = None # 1. STOP-LOSS if profit_pct <= -3.0: exit_reason = "STOP_LOSS" logger.warning(f'🛑 {pair}: STOP-LOSS triggered! {profit_pct:.2f}%') positions_to_close.append((pair, current_qty, current_price, buy_price, buy_qty, profit_pct, exit_reason)) # 2. MAX HOLD TIME elif hold_time_minutes >= 240: exit_reason = "MAX_HOLD_TIMEOUT" logger.info(f'⏱️ {pair}: MAX_HOLD_TIME reached! {hold_time_minutes:.0f} min') positions_to_close.append((pair, current_qty, current_price, buy_price, buy_qty, profit_pct, exit_reason)) # 3. TAKE PROFIT elif profit_pct >= 1.0: exit_reason = "TAKE_PROFIT" logger.info(f'💰 {pair}: TAKE_PROFIT reached! +{profit_pct:.2f}%') positions_to_close.append((pair, current_qty, current_price, buy_price, buy_qty, profit_pct, exit_reason)) # 4. TRAILING STOP elif pos['peak_profit'] >= 1.0: trailing_stop_level = pos['peak_profit'] - 0.4 if profit_pct <= trailing_stop_level: exit_reason = "TRAILING_STOP" logger.info(f'📉 {pair}: TRAILING_STOP triggered! Peak: {pos["peak_profit"]:.2f}%, Current: {profit_pct:.2f}%') positions_to_close.append((pair, current_qty, current_price, buy_price, buy_qty, profit_pct, exit_reason)) except Exception as e: logger.warning(f'Check exit for {pair} failed: {e}') continue # Execute all closes for pair, current_qty, current_price, buy_price, buy_qty, profit_pct, exit_reason in positions_to_close: try: # Format quantity BEFORE placing order (keep as STRING!) if pair in self.symbol_info: step_size = self.symbol_info[pair].get('stepSize', 1e-8) qty_str = self.format_quantity(current_qty, step_size) else: qty_str = f'{current_qty:.8f}' logger.info(f'📤 Placing EXIT order: {qty_str} {pair} (Reason: {exit_reason})') result = await self.binance.place_order( symbol=pair, side='SELL', quantity=qty_str, # Pass STRING order_type='MARKET' ) if result and result.get('status') in ['FILLED', 'NEW', 'PARTIALLY_FILLED']: # ONLY record if order was actually EXECUTED profit_usd = (current_qty * current_price) - (buy_qty * buy_price) self.daily_pnl += profit_usd self.total_pnl += profit_usd self.total_trades += 1 if profit_pct >= 0: self.wins_today += 1 icon = "✅" else: self.losses_today += 1 icon = "❌" # Only send Telegram alert if PROFITABLE (profit_pct >= 0) if profit_pct >= 0: await self.telegram.send_alert( f'{icon} CLOSED {exit_reason}\n' f'{pair}: {current_qty:.8f} @ ${current_price:.2f}\n' f'Profit: ${profit_usd:+.2f} ({profit_pct:+.2f}%)\n' f'Hold: {(datetime.now() - datetime.fromisoformat(self.open_positions[pair]["buy_time"])).total_seconds() / 60:.0f} min' ) else: logger.info(f'Loss trade skipped from Telegram (visible on dashboard): {profit_pct:.2f}%') if self.daily_pnl < self.min_daily_pnl: self.min_daily_pnl = self.daily_pnl if abs(self.min_daily_pnl) > self.max_drawdown: self.max_drawdown = abs(self.min_daily_pnl) self.error_count = 0 # Send to dashboard BEFORE deleting position (ONLY for REAL executed trades!) hold_time_s = (datetime.now() - datetime.fromisoformat(self.open_positions[pair]["buy_time"])).total_seconds() hold_time_min = hold_time_s / 60 await self.dashboard.record_sell(pair, current_qty, current_price, profit_usd, profit_pct, hold_time_min) del self.open_positions[pair] logger.info(f'✅ EXIT EXECUTED & RECORDED TO DASHBOARD!') else: logger.warning(f'❌ EXIT order FAILED or returned no result for {pair} - NOT recording') except Exception as e: logger.warning(f'Exit order failed for {pair}: {e}') continue except Exception as e: logger.error(f'Take profit check error: {e}') async def find_best_trade(self): """Scan multiple pairs for best signal""" try: best_signal = {'pair': None, 'signal': 'HOLD'} for pair in self.pairs: try: # FIX: Robust price fetching try: price = await self.binance.get_ticker_price(pair) if price is None or price <= 0: logger.debug(f'⚠️ Invalid price for {pair}: {price}') continue except Exception as e: logger.debug(f'{pair} price fetch failed: {e}') continue # Get signal try: signal = self.strategy.predict(price) if hasattr(self.strategy, 'predict') else 'BUY' except Exception as e: logger.debug(f'{pair} signal generation failed: {e}') signal = 'HOLD' if signal == 'BUY': logger.info(f'🟢 BUY signal: {pair} at ${price:.2f}') return {'pair': pair, 'price': price, 'signal': signal} except Exception as e: logger.debug(f'{pair} scan failed: {e}') continue return best_signal except Exception as e: logger.error(f'Find trade error: {e}') return {'pair': None, 'signal': 'HOLD'} async def monitor_trades(self): """Main trade monitoring with error handling""" try: self.reset_error_count() if time.time() < self.error_cooldown_until: logger.info('⏸️ ERROR COOLDOWN ACTIVE — pausing 5 minutes') return # 1. Check for SELL opportunities await self.check_take_profit() # 2. Periodically swap small holdings await self.auto_swap_periodically() # 3. Get balance try: balance = await self.binance.get_balance() except Exception as e: logger.error(f'Balance fetch failed: {e}') self.increment_error_count() return usdt = float(balance.get('USDT', {}).get('free', 0)) if balance else 0 # DEBUG: Log FULL balance logger.info(f'💰 Full Balance Breakdown:') for asset, amounts in balance.items(): free = float(amounts.get('free', 0)) locked = float(amounts.get('locked', 0)) if free > 0 or locked > 0: logger.info(f' {asset}: FREE={free:.8f}, LOCKED={locked:.8f}, TOTAL={free+locked:.8f}') self.starting_capital = usdt logger.info(f'📊 Starting capital set: ${self.starting_capital:.2f}') logger.info(f'💰 Balance: {usdt:.2f} USDT | Daily P&L: ${self.daily_pnl:.2f}') # 4. Check DAILY LOSS LIMIT daily_loss_pct = (self.daily_pnl / self.starting_capital) * 100 if self.starting_capital > 0 else 0 if daily_loss_pct <= -5.0: self.daily_loss_limit_reached = True logger.warning(f'🛑 Daily loss limit reached! ({daily_loss_pct:.1f}%) Pausing.') await self.telegram.send_alert( f'🛑 Daily Loss Limit reached!\n' f'P&L: ${self.daily_pnl:.2f} ({daily_loss_pct:.1f}%)\n' f'Bot paused until Midnight UTC' ) return # Reset at midnight UTC now = datetime.utcnow() if now.date() > self.last_daily_reset: self.daily_loss_limit_reached = False self.daily_pnl = 0.0 self.min_daily_pnl = 0.0 self.trades_today = 0 self.wins_today = 0 self.losses_today = 0 self.last_daily_reset = now.date() logger.info('🔄 Daily metrics reset at Midnight UTC') await self.telegram.send_alert('🔄 Daily reset complete — trading resumed') # 5. Find BUY signal if self.daily_loss_limit_reached: logger.info('⏸️ BUY orders paused (loss limit)') return try: trade = await self.find_best_trade() except Exception as e: logger.error(f'Find trade error: {e}') self.increment_error_count() return if trade['signal'] == 'BUY' and usdt >= 5: pair = trade['pair'] price = trade['price'] # Use ALL available capital (not just 50%!) to maximize first trade order_amount = usdt # Use 100% capital qty = self.calculate_valid_quantity(pair, order_amount, price) if qty <= 0: logger.debug(f'⚠️ No valid quantity for {pair}') return notional = qty * price logger.info(f'📈 Order: {qty:.8f} {pair} @ ${price:.2f} = ${notional:.2f}') # SKIP if notional is too small (Binance minimum ~$5 for testing) if notional < 5: logger.warning(f'⏭️ SKIP {pair}: notional ${notional:.2f} < $5 minimum') return try: # Format quantity BEFORE passing to API (keep as STRING!) if pair in self.symbol_info: step_size = self.symbol_info[pair].get('stepSize', 1e-8) qty_str = self.format_quantity(qty, step_size) # STRING! else: qty_str = f'{qty:.8f}' logger.info(f'📤 Placing BUY: {qty_str} {pair} @ ${price:.2f}') try: result = await self.binance.place_order( symbol=pair, side='BUY', quantity=qty_str, # Pass STRING order_type='MARKET' ) logger.info(f'✅ Order result: {result}') except Exception as e: logger.error(f'❌ Order FAILED: {type(e).__name__}: {str(e)}') result = None # ONLY RECORD if order was SUCCESSFUL if result and result.get('status') in ['FILLED', 'NEW', 'PARTIALLY_FILLED']: logger.info(f'🔍 DEBUG: Storing BUY position {pair}: qty={qty}, price={price}') self.open_positions[pair] = { 'qty': qty, 'buy_price': price, 'buy_time': datetime.now().isoformat(), 'peak_profit': 0.0, 'trailing_stop': None, 'order_id': result.get('orderId', 'unknown') } logger.info(f'🔍 DEBUG: After storage, open_positions keys = {list(self.open_positions.keys())}') self.trades_today += 1 self.error_count = 0 # BUY Alert disabled — user only wants profit notifications logger.info(f'✅ BUY FILLED & RECORDED! Order ID: {result.get("orderId", "unknown")}') # Send to dashboard await self.dashboard.record_buy(pair, qty, price) else: # Order FAILED - do NOT record logger.warning(f'❌ Order REJECTED or FAILED - NOT recording in open_positions') except Exception as e: logger.error(f'Trade execution failed: {e}') if self.increment_error_count(): await self.telegram.send_alert('🛑 Too many errors! Bot paused 5 minutes') except Exception as e: logger.error(f'Monitor error: {e}') self.increment_error_count() async def send_performance_report(self): """Send detailed 3-hourly report""" try: self.report_count += 1 balance = await self.binance.get_balance() usdt = float(balance.get('USDT', {}).get('free', 0)) if balance else 0 total_assets_usd = usdt for asset in ['BTC', 'ETH', 'SOL', 'BNB', 'XRP']: try: qty = float(balance.get(asset, {}).get('free', 0)) if balance else 0 if qty > 0: pair = f'{asset}USDT' try: price = await self.binance.get_ticker_price(pair) if price and price > 0: total_assets_usd += qty * price except: pass except: pass real_win_rate = (self.wins_today / (self.wins_today + self.losses_today) * 100) if (self.wins_today + self.losses_today) > 0 else 0 avg_profit = (self.daily_pnl / (self.wins_today + self.losses_today)) if (self.wins_today + self.losses_today) > 0 else 0 daily_loss_pct = (self.daily_pnl / self.starting_capital) * 100 if self.starting_capital > 0 else 0 report = f'''📊 REPORT #{self.report_count} 💹 PORTFOLIO: USDT: ${usdt:.2f} (CHF {usdt * 0.84:.2f}) Total Assets: ${total_assets_usd:.2f} (CHF {total_assets_usd * 0.84:.2f}) 📈 TODAY'S PERFORMANCE: Trades: {self.trades_today} Wins: {self.wins_today} | Losses: {self.losses_today} 📊 REAL METRICS: Win Rate: {real_win_rate:.1f}% Avg P/L per Trade: ${avg_profit:+.2f} (CHF {avg_profit * 0.84:+.2f}) Daily P&L: ${self.daily_pnl:+.2f} (CHF {self.daily_pnl * 0.84:+.2f}) ({daily_loss_pct:+.1f}%) Max Drawdown: ${self.max_drawdown:.2f} (CHF {self.max_drawdown * 0.84:.2f}) 🤖 BOT STATUS: {'🟢 RUNNING' if time.time() >= self.error_cooldown_until else '🟡 ERROR_COOLDOWN'} Open Positions: {len(self.open_positions)} Error Count: {self.error_count}/{self.error_threshold}''' logger.info(report) await self.telegram.send_alert(report) # Send all metrics to dashboard balance = await self.binance.get_balance() usdt = float(balance.get('USDT', {}).get('free', 0)) if balance else 0 total_assets_usd = usdt for asset in ['BTC', 'ETH', 'SOL', 'BNB', 'XRP']: try: qty = float(balance.get(asset, {}).get('free', 0)) if balance else 0 if qty > 0: pair = f'{asset}USDT' price = await self.binance.get_ticker_price(pair) if price and price > 0: total_assets_usd += qty * price except: pass await self.dashboard.update_state( balance={'USDT': usdt}, daily_pnl=self.daily_pnl, total_pnl=self.total_pnl, trades_today=self.trades_today, wins_today=self.wins_today, losses_today=self.losses_today, portfolio_value_usd=total_assets_usd ) except Exception as e: logger.error(f'Report error: {e}') async def cancel_all_open_orders(self): """Cancel ALL open orders to free up locked capital""" try: logger.info('🗑️ CANCELLING ALL OPEN ORDERS...') # Get all open orders open_orders = await self.binance._get('openOrders') if not open_orders: logger.info('✅ No open orders to cancel') return True logger.warning(f'⚠️ Found {len(open_orders)} open orders!') cancelled_count = 0 for order in open_orders: try: symbol = order.get('symbol') order_id = order.get('orderId') side = order.get('side') qty = order.get('origQty') logger.warning(f' Cancelling: {symbol} {side} {qty} (Order {order_id})') result = await self.binance._delete( 'order', True, symbol=symbol, orderId=order_id ) cancelled_count += 1 logger.info(f' ✅ Cancelled: {symbol} {order_id}') except Exception as e: logger.error(f' ❌ Failed to cancel {symbol} {order_id}: {e}') continue logger.info(f'✅ CANCELLATION COMPLETE: {cancelled_count}/{len(open_orders)} orders cancelled') return True except Exception as e: logger.error(f'❌ Failed to cancel orders: {e}') return False async def run(self): """Main bot loop""" logger.info('🤖 BOT STARTED (V5 - SUSTAINABLE)') # Create trigger file location trigger_file = '/tmp/bot_liquidate_trigger' # FIRST: Cancel all pending orders to free capital await self.cancel_all_open_orders() # SECOND: Load exchange info BEFORE liquidation await self.load_exchange_info() # THIRD: ONE-TIME LIQUIDATE BTC TO USDT IF NEEDED await self.liquidate_btc_to_usdt_on_startup() logger.info('🗑️ RESET: Clearing dashboard cache...') try: await self.dashboard.clear_state() except: pass await self.telegram.send_alert( '🤖 BOT V5 SUSTAINABLE STARTED\n' '✅ All Bugs Fixed:\n' ' • Price fetching robust\n' ' • SWAP errors handled\n' ' • BUY orders executing\n' ' • EXIT orders scheduled\n' ' • Error resilience active' ) while True: try: # CHECK FOR LIQUIDATION TRIGGER FILE (every cycle) trigger_file = '/tmp/bot_liquidate_trigger' trigger_exists = os.path.exists(trigger_file) logger.info(f'🔍 Checking trigger file: exists={trigger_exists}') # DEBUG if trigger_exists: logger.warning(f'🔥🔥🔥 LIQUIDATION TRIGGER DETECTED! File exists at: {trigger_file}') try: os.remove(trigger_file) logger.warning(f'🔥 Removed trigger file') except Exception as e: logger.error(f'Could not remove trigger: {e}') result = await self.force_liquidate_all() logger.warning(f'🔥 Force liquidation result: {result}') await asyncio.sleep(2) current_time = time.time() # KONTINUIERLICH: Update dashboard mit aktueller Balance (every cycle!) try: balance = await self.binance.get_balance() usdt_live = float(balance.get('USDT', {}).get('free', 0)) if balance else 0 # Calculate portfolio value with REAL prices from symbol_info portfolio_value_usd = usdt_live # Start with USDT for pair in self.pairs: asset = pair.replace('USDT', '') if asset in balance: qty = float(balance[asset].get('free', 0)) if qty > 0 and pair in self.symbol_info: # Use current price from symbol_info or last known try: current_price = await self.binance.get_ticker_price(pair) if current_price and current_price > 0: portfolio_value_usd += qty * current_price except: pass logger.info(f'✅ Sending to dashboard: USDT={usdt_live:.2f}, portfolio={portfolio_value_usd:.2f}, trades_today={self.trades_today}, open_trades={len(self.open_positions)}') # SYNC open_positions with dashboard async with aiohttp.ClientSession() as session: async with session.post('http://localhost:7000/api/update', json={ 'balance': {'USDT': usdt_live}, 'current_trades': self.open_positions, # Send ALL open positions! 'daily_pnl': self.daily_pnl, 'total_pnl': self.total_pnl, 'trades_today': self.trades_today, 'wins_today': self.wins_today, 'losses_today': self.losses_today, 'portfolio_value_usd': portfolio_value_usd, 'portfolio_value_chf': portfolio_value_usd * 0.84, 'last_update': datetime.now().isoformat() }) as resp: if resp.status == 200: logger.debug('✅ Dashboard state synced') else: logger.warning(f'Dashboard sync failed: {resp.status}') # Keep the old update_state call for compatibility await self.dashboard.update_state( balance={'USDT': usdt_live}, daily_pnl=self.daily_pnl, portfolio_value_usd=portfolio_value_usd, total_pnl=self.total_pnl, trades_today=self.trades_today, wins_today=self.wins_today, losses_today=self.losses_today ) except Exception as e: logger.warning(f'⚠️ Dashboard update error: {e}') now = datetime.now() should_report = (now.hour in [22, 1, 4, 7, 10, 13, 16, 19]) and now.minute == 0 if should_report and (current_time - self.last_report_time) > 60: await self.send_performance_report() self.last_report_time = current_time await self.monitor_trades() await asyncio.sleep(10) # Check every 10 seconds for trigger and trading signals except Exception as e: logger.error(f'Main loop error: {e}') await asyncio.sleep(60) async def main(): logger.info('▶️ MAIN STARTUP') try: config = get_config() logger.info(f'✅ Config loaded') try: dashboard = DashboardClient() logger.info(f'✅ Dashboard client initialized') except Exception as e: logger.error(f'❌ Dashboard init failed: {e}') dashboard = None binance = BinanceClientWrapper( api_key=config.binance_api_key_live, api_secret=config.binance_api_secret_live, testnet=False ) logger.info(f'✅ Binance client initialized') telegram = TelegramNotifier(bot_token=config.telegram_bot_token, chat_id=config.telegram_chat_id) obsidian = ObsidianLogger(vault_path=config.obsidian_vault_path, trade_log_file=config.obsidian_trade_log_file) model = joblib.load(config.model_path) if hasattr(config, 'model_path') else None scaler = None logger.info(f'✅ BOT CREATING...') bot = MLTradingBot(config, binance, telegram, obsidian, model, scaler) logger.info(f'✅ BOT CREATED. STARTING RUN()...') await bot.run() except Exception as e: logger.critical(f'❌ FATAL ERROR IN MAIN: {e}', exc_info=True) if __name__ == '__main__': asyncio.run(main()) # Version marker: Auto-sync test Sat Jul 4 10:37:55 UTC 2026