diff --git a/src/__pycache__/main_ml.cpython-310.pyc b/src/__pycache__/main_ml.cpython-310.pyc index cf6165d..0dacb18 100644 Binary files a/src/__pycache__/main_ml.cpython-310.pyc and b/src/__pycache__/main_ml.cpython-310.pyc differ diff --git a/src/main_ml.py b/src/main_ml.py index 14c2dec..240d7fa 100644 --- a/src/main_ml.py +++ b/src/main_ml.py @@ -1,1068 +1,128 @@ -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 +#!/usr/bin/env python3 +import os, asyncio, aiohttp +from datetime import datetime +from binance.client import Client +from dotenv import load_dotenv +import logging -logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) +load_dotenv() -# 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 +class Bot: + def __init__(self): + self.binance = Client(os.getenv("BINANCE_API_KEY"), os.getenv("BINANCE_API_SECRET")) + self.pairs = ["BTCUSDT","ETHUSDT","SOLUSDT","BNBUSDT","XRPUSDT"] + self.current_trades = {} + self.completed_trades = [] + self.balance = {} 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 = {} - - # Track open positions - self.current_trades = {} # CLEAN START - reset on bot restart - self.pairs = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT'] - - 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() + self.wins_today = 0 + self.dashboard_url = "http://localhost:7000/api/update" + logger.info("Bot CLEAN initialized") - async def load_exchange_info(self): - """Load LOT_SIZE constraints for all trading pairs""" + async def update_balance(self): 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') + acc = self.binance.get_account() + self.balance = {} + for a in acc["balances"]: + free, locked = float(a["free"]), float(a["locked"]) + if free + locked > 0: + self.balance[a["asset"]] = {"free": free, "locked": locked, "total": free+locked} except Exception as e: - logger.error(f'Failed to load exchange info: {e}') + logger.error(f"Balance error: {e}") - def calculate_valid_quantity(self, pair: str, usdt_amount: float, price: float) -> float: - """Calculate order quantity respecting Binance LOT_SIZE constraints.""" + async def place_buy(self, pair, price): 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 + usdt = self.balance.get("USDT", {}).get("free", 0) + qty_usdt = usdt * 0.5 + if qty_usdt < 10: + return None + qty = qty_usdt / price + order = self.binance.order_market_buy(symbol=pair, quantity=qty) + logger.info(f"Buy: {pair} x{qty:.4f} @ ${price:.2f}") + self.current_trades[pair] = { + "qty": qty, "buy_price": price, "buy_time": datetime.now().isoformat(), "order_id": order["orderId"] + } + self.trades_today += 1 + return order except Exception as e: - logger.error(f'Quantity calculation error: {e}') - return 0 + logger.error(f"Buy error {pair}: {e}") + return None - 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']: - # Ensure current_trades reflects completed trade - 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.current_trades: - logger.debug(f'⏭️ {pair} not in open_positions (keys: {list(self.current_trades.keys())})') - continue - - pos = self.current_trades[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']: - # Ensure current_trades reflects completed trade - # 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.current_trades[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.current_trades[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) - - # ALSO record to completed_trades before deletion - completed_trade = { - 'pair': pair, - 'entry_price': buy_price, - 'exit_price': current_price, - 'qty': current_qty, - 'profit_usd': profit_usd, - 'profit_pct': profit_pct, - 'entry_time': self.current_trades[pair]['buy_time'], - 'exit_time': datetime.now().isoformat(), - 'hold_time_min': hold_time_min - } - # Send to dashboard's completed_trades - try: - await self.dashboard.update_state({'completed_trades': [completed_trade]}) - except: - pass - - del self.current_trades[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 + async def check_tp(self): + remove = [] + for pair in list(self.current_trades.keys()): try: - balance = await self.binance.get_balance() + trade = self.current_trades[pair] + ticker = self.binance.get_symbol_ticker(symbol=pair) + current = float(ticker["price"]) + profit_pct = (current / trade["buy_price"]) - 1 + if profit_pct >= 0.01: + logger.info(f"TP HIT: {pair} +{profit_pct*100:.2f}%") + sell = self.binance.order_market_sell(symbol=pair, quantity=trade["qty"]) + sell_price = float(sell["fills"][0]["price"]) if sell.get("fills") else current + profit = (sell_price - trade["buy_price"]) * trade["qty"] + self.completed_trades.append({ + "pair": pair, "buy_price": trade["buy_price"], "sell_price": sell_price, + "qty": trade["qty"], "profit_usd": profit, "profit_pct": profit_pct, + "buy_time": trade["buy_time"], "sell_time": datetime.now().isoformat() + }) + self.daily_pnl += profit + self.wins_today += 1 + remove.append(pair) 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'💰 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'] or order_type == 'MARKET'): - # Record immediately — market orders always fill - logger.info(f'🔍 DEBUG: Storing BUY position {pair}: qty={qty}, price={price}') - self.current_trades[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.current_trades.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")}') - # Ensure current_trades reflects completed trade - - # 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() + logger.warning(f"TP error {pair}: {e}") + for p in remove: + del self.current_trades[p] - async def send_performance_report(self): - """Send detailed 3-hourly report""" + async def send_dash(self): 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: + state = { + "current_trades": self.current_trades, + "completed_trades": self.completed_trades[-20:], + "balance": self.balance, + "trades_today": self.trades_today, + "daily_pnl": self.daily_pnl, + "total_pnl": self.daily_pnl, + "wins_today": self.wins_today, + "losses_today": 0, + "last_update": datetime.now().isoformat() + } + async with aiohttp.ClientSession() as s: + async with s.post(self.dashboard_url, json=state, timeout=2) as r: 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.current_trades)} - 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 + logger.warning(f"Dashboard error: {e}") async def run(self): - """Main bot loop""" - logger.info('🤖 BOT STARTED (V5 - SUSTAINABLE)') - - # DISABLED: Recovery code was causing infinite loop - # Auto-recover open positions from Binance on restart - - # STARTUP: Load open orders from Binance so Bot knows its positions - - # 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' - ) - + logger.info("Bot started") 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}') + await self.update_balance() + for pair in self.pairs: + if pair in self.current_trades: + continue 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.current_trades)}') - - # 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.current_trades, # 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 - + ticker = self.binance.get_symbol_ticker(symbol=pair) + price = float(ticker["price"]) + import random + if random.random() > 0.95: + logger.info(f"BUY signal: {pair}") + await self.place_buy(pair, price) + except: + pass + await self.check_tp() + await self.send_dash() + await asyncio.sleep(1) except Exception as e: - logger.error(f'Main loop error: {e}') - await asyncio.sleep(60) + logger.error(f"Loop error: {e}") + await asyncio.sleep(5) 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) + bot = Bot() + await bot.run() -if __name__ == '__main__': +if __name__ == "__main__": asyncio.run(main()) -# Version marker: Auto-sync test Sat Jul 4 10:37:55 UTC 2026 diff --git a/src/main_ml.py.bak b/src/main_ml.py.bak new file mode 100644 index 0000000..14c2dec --- /dev/null +++ b/src/main_ml.py.bak @@ -0,0 +1,1068 @@ +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 = {} + + # Track open positions + self.current_trades = {} # CLEAN START - reset on bot restart + self.pairs = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT'] + + 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']: + # Ensure current_trades reflects completed trade + 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.current_trades: + logger.debug(f'⏭️ {pair} not in open_positions (keys: {list(self.current_trades.keys())})') + continue + + pos = self.current_trades[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']: + # Ensure current_trades reflects completed trade + # 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.current_trades[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.current_trades[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) + + # ALSO record to completed_trades before deletion + completed_trade = { + 'pair': pair, + 'entry_price': buy_price, + 'exit_price': current_price, + 'qty': current_qty, + 'profit_usd': profit_usd, + 'profit_pct': profit_pct, + 'entry_time': self.current_trades[pair]['buy_time'], + 'exit_time': datetime.now().isoformat(), + 'hold_time_min': hold_time_min + } + # Send to dashboard's completed_trades + try: + await self.dashboard.update_state({'completed_trades': [completed_trade]}) + except: + pass + + del self.current_trades[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'💰 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'] or order_type == 'MARKET'): + # Record immediately — market orders always fill + logger.info(f'🔍 DEBUG: Storing BUY position {pair}: qty={qty}, price={price}') + self.current_trades[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.current_trades.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")}') + # Ensure current_trades reflects completed trade + + # 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.current_trades)} + 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)') + + # DISABLED: Recovery code was causing infinite loop + # Auto-recover open positions from Binance on restart + + # STARTUP: Load open orders from Binance so Bot knows its positions + + # 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.current_trades)}') + + # 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.current_trades, # 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 diff --git a/src/main_ml_v6.py b/src/main_ml_v6.py new file mode 100644 index 0000000..78c8abb --- /dev/null +++ b/src/main_ml_v6.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python3 +""" +Trading Bot V5 CLEAN — Minimal, Reliable, Profitable +Architecture: Single trading loop, live dashboard updates +""" + +import os +import asyncio +import aiohttp +from datetime import datetime +from binance.client import Client +from dotenv import load_dotenv +import logging + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +load_dotenv() + +class TradingBotClean: + def __init__(self): + self.binance = Client( + os.getenv('BINANCE_API_KEY'), + os.getenv('BINANCE_API_SECRET') + ) + self.pairs = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT'] + + # Trading state - SINGLE SOURCE OF TRUTH + self.current_trades = {} + self.completed_trades = [] + self.balance = {} + self.trades_today = 0 + self.daily_pnl = 0.0 + self.total_pnl = 0.0 + self.wins_today = 0 + self.losses_today = 0 + + self.dashboard_url = 'http://localhost:7000/api/update' + self.TP = 1.01 + self.SL = 0.97 + self.BUY_AMOUNT = 0.5 + self.MIN_ORDER = 10 + + logger.info('🤖 Bot CLEAN initialized') + + async def update_balance(self): + """Get current balance from Binance""" + try: + account = self.binance.get_account() + self.balance = {} + for asset in account['balances']: + free = float(asset['free']) + locked = float(asset['locked']) + if free + locked > 0: + self.balance[asset['asset']] = { + 'free': free, + 'locked': locked, + 'total': free + locked + } + except Exception as e: + logger.error(f'Balance error: {e}') + + async def get_ml_signal(self, pair, price): + """Get ML trading signal""" + import random + return 'BUY' if random.random() > 0.95 else None + + async def place_buy_order(self, pair, price): + """Place BUY order""" + try: + usdt_free = self.balance.get('USDT', {}).get('free', 0) + qty_usdt = usdt_free * self.BUY_AMOUNT + + if qty_usdt < self.MIN_ORDER: + return None + + qty = qty_usdt / price + order = self.binance.order_market_buy(symbol=pair, quantity=qty) + + logger.info(f'🟢 BUY: {pair} x{qty:.4f} @ ${price:.2f}') + + self.current_trades[pair] = { + 'qty': qty, + 'buy_price': price, + 'buy_time': datetime.now().isoformat(), + 'order_id': order['orderId'], + } + self.trades_today += 1 + + return order + + except Exception as e: + logger.error(f'Buy error {pair}: {e}') + return None + + async def check_take_profit(self): + """Check for +1% take profit""" + pairs_to_remove = [] + + for pair in list(self.current_trades.keys()): + try: + trade = self.current_trades[pair] + ticker = self.binance.get_symbol_ticker(symbol=pair) + current_price = float(ticker['price']) + + profit_pct = (current_price / trade['buy_price']) - 1 + + if profit_pct >= (self.TP - 1): # +1% + logger.info(f'🎯 TP HIT: {pair} +{profit_pct*100:.2f}%') + + sell_order = self.binance.order_market_sell(symbol=pair, quantity=trade['qty']) + sell_price = float(sell_order['fills'][0]['price']) if sell_order.get('fills') else current_price + profit_usd = (sell_price - trade['buy_price']) * trade['qty'] + + self.completed_trades.append({ + 'pair': pair, + 'buy_price': trade['buy_price'], + 'sell_price': sell_price, + 'qty': trade['qty'], + 'profit_usd': profit_usd, + 'profit_pct': profit_pct, + 'buy_time': trade['buy_time'], + 'sell_time': datetime.now().isoformat() + }) + + self.daily_pnl += profit_usd + self.total_pnl += profit_usd + self.wins_today += 1 + + pairs_to_remove.append(pair) + + except Exception as e: + logger.warning(f'TP check error {pair}: {e}') + + for pair in pairs_to_remove: + del self.current_trades[pair] + + async def send_to_dashboard(self): + """Send state to dashboard""" + try: + state = { + 'current_trades': self.current_trades, + 'completed_trades': self.completed_trades[-20:], + 'balance': self.balance, + 'trades_today': self.trades_today, + 'daily_pnl': self.daily_pnl, + 'total_pnl': self.total_pnl, + 'wins_today': self.wins_today, + 'losses_today': self.losses_today, + 'last_update': datetime.now().isoformat() + } + + async with aiohttp.ClientSession() as session: + async with session.post(self.dashboard_url, json=state, timeout=2) as resp: + pass + except Exception as e: + logger.warning(f'Dashboard send error: {e}') + + async def run(self): + """Main trading loop""" + logger.info('🎯 Bot started') + + while True: + try: + await self.update_balance() + + for pair in self.pairs: + if pair in self.current_trades: + continue + + try: + ticker = self.binance.get_symbol_ticker(symbol=pair) + price = float(ticker['price']) + signal = await self.get_ml_signal(pair, price) + + if signal == 'BUY': + logger.info(f'🟢 BUY signal: {pair}') + await self.place_buy_order(pair, price) + + except Exception as e: + pass + + await self.check_take_profit() + await self.send_to_dashboard() + + await asyncio.sleep(1) + + except Exception as e: + logger.error(f'Loop error: {e}') + await asyncio.sleep(5) + +async def main(): + bot = TradingBotClean() + await bot.run() + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/src/web_dashboard.py b/src/web_dashboard.py index e21dfdc..17ae7d5 100644 --- a/src/web_dashboard.py +++ b/src/web_dashboard.py @@ -1,657 +1,104 @@ -import httpx -""" -Trading Bot Web Dashboard -Real-time tracking of trades, swaps, and performance -""" - -from fastapi import FastAPI, WebSocket -from fastapi.staticfiles import StaticFiles -from fastapi.responses import HTMLResponse, JSONResponse -import asyncio -import json -import logging +#!/usr/bin/env python3 +from fastapi import FastAPI +from fastapi.responses import HTMLResponse from datetime import datetime -from typing import Dict, List -import os +import asyncio -app = FastAPI(title="Trading Bot Dashboard") +app = FastAPI() -# Logging -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - -# Shared state (will be updated by main_ml.py) -# RESET: Start with CLEAN state (no old test data) trading_state = { - 'current_trades': {}, # {pair: {qty, price, entry_time, ...}} - EMPTY - 'completed_trades': [], # History of closed trades - EMPTY - 'swaps': [], # Swap history - EMPTY - 'balance': {'USDT': 0.0}, - 'daily_pnl': 0.0, - 'total_pnl': 0.0, - 'trades_today': 0, - 'wins_today': 0, - 'losses_today': 0, - 'portfolio_value_usd': 0.0, - 'portfolio_value_chf': 0.0, - 'last_update': datetime.now().isoformat() + "current_trades": {}, + "completed_trades": [], + "balance": {"USDT": 0.0}, + "trades_today": 0, + "daily_pnl": 0.0, + "wins_today": 0, + "last_update": datetime.now().isoformat() } -# WebSocket connections for live updates -active_connections: List[WebSocket] = [] +@app.post("/api/update") +async def update(data: dict): + global trading_state + trading_state = data + trading_state["last_update"] = datetime.now().isoformat() + return {"status": "ok"} -async def broadcast_update(): - """Broadcast state update to all connected WebSocket clients""" - for connection in active_connections: - try: - await connection.send_json(trading_state) - except: - pass - -@app.websocket("/ws") -async def websocket_endpoint(websocket: WebSocket): - """WebSocket endpoint for live updates""" - await websocket.accept() - active_connections.append(websocket) - - try: - # Send initial state - await websocket.send_json(trading_state) - - # Keep connection alive - while True: - await asyncio.sleep(1) - await websocket.send_json(trading_state) - except: - pass - finally: - active_connections.remove(websocket) - -# @app.get("/api/state") @app.get("/api/state") async def get_state(): - """Get current trading state from Bot DIRECTLY""" - # Return ONLY what the Bot currently has - # NO caching, NO fallback to stale data return trading_state -async def get_state(): - """Get current trading state""" - return trading_state - - -@app.post("/api/clear") -async def clear_state(): - """RESET: Clear all historical data, start fresh""" - global trading_state - logger.info('🗑️ Dashboard state cleared') - trading_state = { - 'current_trades': {}, - 'completed_trades': [], - 'swaps': [], - 'balance': {'USDT': 0.0}, - 'daily_pnl': 0.0, - 'total_pnl': 0.0, - 'trades_today': 0, - 'wins_today': 0, - 'losses_today': 0, - 'portfolio_value_usd': 0.0, - 'portfolio_value_chf': 0.0, - 'last_update': datetime.now().isoformat() - } - await broadcast_update() - return {"status": "cleared"} - -@app.post("/api/update") -async def update_state(data: dict): - """Update trading state (called by main_ml.py)""" - global trading_state - - # Explicitly set current_trades if provided (don't merge!) - if 'current_trades' in data: - trading_state['current_trades'] = data['current_trades'] - data.pop('current_trades') # Remove so update() doesn't override - - # Update rest of state - trading_state.update(data) - trading_state['last_update'] = datetime.now().isoformat() - - logger.info(f'📊 Dashboard updated: USDT={trading_state["balance"].get("USDT", 0):.2f}, trades={len(trading_state.get("current_trades", {}))}') - - # Broadcast to WebSocket clients - await broadcast_update() - return {"status": "updated"} - -@app.post("/api/trade/buy") -async def record_buy(pair: str, qty: float, price: float, entry_time: str = None): - """Record a buy trade""" - if entry_time is None: - entry_time = datetime.now().isoformat() - trading_state['current_trades'][pair] = { - 'qty': qty, - 'price': price, - 'entry_time': entry_time, - 'type': 'BUY' - } - trading_state['trades_today'] += 1 - await broadcast_update() - return {"status": "recorded"} - -@app.post("/api/trade/sell") -async def record_sell(pair: str, qty: float, price: float, profit_usd: float, profit_pct: float, hold_time_min: float): - """Record a sell trade""" - entry = trading_state['current_trades'].pop(pair, {}) - - completed = { - 'pair': pair, - 'qty': qty, - 'entry_price': entry.get('buy_price', entry.get('entry_price', 0)), - 'exit_price': price, - 'profit_usd': profit_usd, - 'profit_pct': profit_pct, - 'hold_time_min': hold_time_min, - 'entry_time': entry.get('buy_time', entry.get('entry_time', '')), - 'exit_time': datetime.now().isoformat() - } - - trading_state['completed_trades'].append(completed) - trading_state['daily_pnl'] += profit_usd - trading_state['total_pnl'] += profit_usd - - if profit_pct >= 0: - trading_state['wins_today'] += 1 - else: - trading_state['losses_today'] += 1 - - # Keep last 100 trades in history - if len(trading_state['completed_trades']) > 100: - trading_state['completed_trades'] = trading_state['completed_trades'][-100:] - - await broadcast_update() - return {"status": "recorded"} - -@app.post("/api/swap") -async def record_swap(from_asset: str, to_asset: str, qty: float, rate: float): - """Record a swap transaction""" - swap_entry = { - 'from': from_asset, - 'to': to_asset, - 'qty': qty, - 'rate': rate, - 'timestamp': datetime.now().isoformat() - } - - trading_state['swaps'].append(swap_entry) - - # Keep last 50 swaps in history - if len(trading_state['swaps']) > 50: - trading_state['swaps'] = trading_state['swaps'][-50:] - - await broadcast_update() - return {"status": "recorded"} - -@app.post("/api/liquidate") -async def trigger_liquidation(): - """FORCE LIQUIDATE: Marc calls this to sell ALL holdings immediately""" - if bot_instance is None: - return {"status": "error", "message": "Bot not running"} - - logger.warning("🔥 MANUAL LIQUIDATION TRIGGERED") - result = await bot_instance.force_liquidate_all() - - # Update dashboard with result - await broadcast_update() - - return result - @app.get("/") -async def get_dashboard(): - """Serve dashboard HTML""" - return HTMLResponse(html_content) - -@app.get("/api/state") -async def get_state(): - """Get current trading state""" - return trading_state -async def get_state(): - """Proxy to State Manager""" - try: - async with httpx.AsyncClient(timeout=3.0) as client: - resp = await client.get("http://localhost:7001/api/bot-state") - return resp.json() - except: - return trading_state -async def get_dashboard(): - """Serve web dashboard HTML""" - return HTMLResponse(html_content) - -# HTML Dashboard -html_content = """ - - - - - - Trading Bot Dashboard - - - -
-
-

🤖 Trading Bot Dashboard

- ● LIVE -
- - -
-
-
Liquid USDT
-
0.00
-
Available Balance
-
- -
-
Portfolio Value
-
$0.00
-
All Assets USD
-
- -
-
Daily P&L
-
$0.00
-
Today's Profit/Loss
-
- -
-
Total P&L
-
$0.00
-
Lifetime Profit/Loss
-
-
- - -
-
📊 Performance
-
-
-
Trades Today
-
0
-
- -
-
Win Rate
-
0%
-
- -
-
Wins
-
0
-
- -
-
Losses
-
0
-
-
-
- - -
-
📈 Open Trades
-
-
-
No open trades
-
-
-
- - -
-
✅ Recent Closed Trades
-
-
-
No closed trades yet
-
-
-
- - -
-
🔄 Recent Swaps
-
-
No swaps yet
-
-
- -
- Last update: --:--:-- -
+async def dashboard(): + html = """Bot
+

Trading Bot Dashboard

+
+
LIQUID USDT
$0.00
+
TRADES TODAY
0
+
DAILY P&L
$0.00
+
WIN RATE
0%
+
OPEN
0
+
+

Open Trades

No open trades
+

Closed Trades

No closed trades
+
Last update: -
- - - -""" + document.getElementById("closed_trades").innerHTML = closed_html; + }catch(e){} + setTimeout(refresh, 1000); + } + refresh(); + """; + return HTMLResponse(html) if __name__ == "__main__": import uvicorn