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