diff --git a/src/main_ml.py b/src/main_ml.py index 0d42508..97b70a1 100644 --- a/src/main_ml.py +++ b/src/main_ml.py @@ -1,246 +1,245 @@ #!/usr/bin/env python3 -""" -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 -import time -import logging +'''Trading Bot v0.4 - Dynamic Position Sizing''' +import os, json, time, logging from datetime import datetime from dotenv import load_dotenv from binance.client import Client from binance.exceptions import BinanceAPIException - -# Setup -logging.basicConfig(level=logging.INFO, format='%(levelname)s:%(message)s') +logging.basicConfig(level=logging.INFO, format='%(asctime)s | %(message)s') logger = logging.getLogger() load_dotenv() -try: - API_KEY = os.getenv('BINANCE_API_KEY_LIVE') - API_SECRET = os.getenv('BINANCE_API_SECRET_LIVE') -except: +API_KEY = os.getenv('BINANCE_API_KEY_LIVE') +API_SECRET = os.getenv('BINANCE_API_SECRET_LIVE') + +if not API_KEY or not API_SECRET: logger.error("Missing API keys") exit(1) -# Constants +# CONSTANTS - DYNAMIC SIZING SYMBOLS = ['BTCUSDT', 'ETHUSDT', 'BNBUSDT', 'XRPUSDT', 'SOLUSDT'] +TRACKED_COINS = ['BTC', 'ETH', 'BNB', 'XRP', 'SOL'] MIN_USDT = 5.00 -MAX_TRADE_USDT = 20.00 -TAKE_PROFIT_PCT = 0.015 # +1.5% -STOP_LOSS_PCT = -0.008 # -0.8% +MAX_POSITION_PCT = 0.05 +TAKE_PROFIT_PCT = 0.015 +STOP_LOSS_PCT = -0.008 CYCLE_SEC = 60 - -class TradingBotV03: - """Trading Bot with Fresh Cache + Local Min Signals + Hard Risk Management""" - +class TradingBotV04: def __init__(self): 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") + self.active_trades = {} + self.portfolio_value = 0 + self.max_trade_usdt = 0 + logger.info("[v0.4 INIT] Bot initialized | Dynamic Position Sizing") def get_fresh_balance(self): - """KEY FIX: Always fetch FRESH balance from API (no stale cache!)""" try: account = self.client.get_account() - 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 + portfolio_value = 0 + + prices = {'USDT': 1.0} + for symbol in SYMBOLS: + try: + ticker = self.client.get_ticker(symbol=symbol) + coin = symbol.replace('USDT', '') + prices[coin] = float(ticker['lastPrice']) + except: + pass + + for balance in account['balances']: + asset = balance['asset'] + free = float(balance['free']) + + if asset in TRACKED_COINS: + price = prices.get(asset, 0) + portfolio_value += free * price + elif asset == 'USDT': + portfolio_value += free + + usdt_available = next( + (float(b['free']) for b in account['balances'] if b['asset'] == 'USDT'), + 0 + ) + + self.portfolio_value = portfolio_value + self.max_trade_usdt = max(MIN_USDT, portfolio_value * MAX_POSITION_PCT) + + logger.info(f"[v0.4] USDT=${usdt_available:.2f} | Portfolio=${portfolio_value:.2f} | MaxTrade=${self.max_trade_usdt:.2f}") + return usdt_available, portfolio_value + except BinanceAPIException as e: logger.error(f"Balance fetch failed: {e}") - return {}, 0 + return 0, 0 def get_current_price(self, symbol): - """Get current market price""" try: - trades = self.client.get_recent_trades(symbol=symbol, limit=1) - if trades: - return float(trades[0]['price']) - return None + ticker = self.client.get_ticker(symbol=symbol) + return float(ticker['lastPrice']) except: return None def calculate_valid_quantity(self, symbol, usdt_amount): - """Calculate valid order quantity respecting LOT_SIZE""" try: price = self.get_current_price(symbol) - if not price: + if not price or price <= 0: return 0 info = self.client.get_symbol_info(symbol) if not info: return 0 - step_size = 0.00001 # default - for filt in info.get('filters', []): - if filt['filterType'] == 'LOT_SIZE': - step_size = float(filt['stepSize']) + step_size = None + for f in info.get('filters', []): + if f['filterType'] == 'LOT_SIZE': + step_size = float(f['stepSize']) break - qty = (usdt_amount / price) - qty = int(qty / step_size) * step_size # Round to step_size - notional = qty * price + if not step_size or step_size <= 0: + return 0 - if notional < MIN_USDT: - logger.debug(f"Order too small: {symbol} ${notional:.2f}") + qty = usdt_amount / price + qty = int(qty / step_size) * step_size + + if qty * price < 5.0: return 0 return qty - except Exception as e: - logger.warning(f"Qty calc failed: {e}") + except: return 0 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]) + recent = self.price_history[symbol][-5:] + current = recent[-1] + is_min = all(current < p for p in recent[:-1]) if is_min: - logger.info(f"Local min detected: {symbol} @ ${current_price:.2f}") + logger.info(f"[SIGNAL] Local min: {symbol} @ ${current:.2f}") return is_min 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: + if qty <= 0: return None - entry_price = self.get_current_price(symbol) - if not entry_price: + price = self.get_current_price(symbol) + if not 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, + 'entry_price': price, 'qty': qty, 'order_id': order.get('orderId'), - 'entry_time': datetime.now() + 'entry_time': datetime.now().isoformat() } - 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}%)") + pos_pct = (qty * price / self.portfolio_value * 100) if self.portfolio_value > 0 else 0 + logger.info(f"[BUY] {symbol} {qty} @ ${price:.2f} | Position: {pos_pct:.1f}% | [v0.4 DYNAMIC]") return order - except BinanceAPIException as e: - logger.error(f"Buy order failed: {e}") + logger.error(f"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 + for symbol, trade in list(self.active_trades.items()): + try: + current = self.get_current_price(symbol) + if not current: + continue + + entry = trade['entry_price'] + qty = trade['qty'] + pnl_pct = ((current - entry) / entry) * 100 + + if pnl_pct >= TAKE_PROFIT_PCT * 100: + logger.info(f"[SELL-TP] {symbol} @ ${current:.2f} | +{pnl_pct:.2f}%") + try: + self.client.order_market_sell(symbol=symbol, quantity=qty) + del self.active_trades[symbol] + except: + pass + + elif pnl_pct <= STOP_LOSS_PCT * 100: + logger.info(f"[SELL-SL] {symbol} @ ${current:.2f} | {pnl_pct:.2f}%") + try: + self.client.order_market_sell(symbol=symbol, quantity=qty) + del self.active_trades[symbol] + except: + pass + except: + pass - def cycle(self): - """Main trading cycle (runs every 60 seconds)""" + def run_cycle(self): 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() + usdt_free, portfolio_val = 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) + trade_amount = min(self.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) + logger.info(f"[CYCLE-END] Trades: {len(self.active_trades)} | USDT: ${usdt_free:.2f} | Portfolio: ${portfolio_val:.2f} [v0.4]") try: - while True: - self.cycle() - time.sleep(CYCLE_SEC) - except KeyboardInterrupt: - logger.info("Bot stopped by user") + temp = '/home/marc/bot-deploy/active_trades.json.tmp' + with open(temp, 'w') as f: + json.dump({ + 'active_trades': self.active_trades, + 'count': len(self.active_trades), + 'portfolio_value': round(portfolio_val, 2), + 'max_trade_usdt': round(self.max_trade_usdt, 2), + 'timestamp': datetime.now().isoformat(), + 'version': 'v0.4-dynamic' + }, f) + os.replace(temp, '/home/marc/bot-deploy/active_trades.json') except Exception as e: - logger.error(f"CRITICAL ERROR: {e}") - raise - + logger.warning(f"Save failed: {e}") + + logger.info("=" * 70) if __name__ == '__main__': - bot = TradingBotV03() - bot.run() + import sys + + bot = TradingBotV04() + + if len(sys.argv) > 1 and sys.argv[1] == '--once': + bot.run_cycle() + else: + logger.info("[v0.4 START] Trading Bot cycle loop running...") + while True: + try: + bot.run_cycle() + except Exception as e: + logger.error(f"Cycle error: {e}") + + time.sleep(CYCLE_SEC)