#!/usr/bin/env python3 """Trading Bot v0.4 Hybrid - Dynamic Position Sizing + Trade Recovery""" import os, json, time, logging from datetime import datetime from dotenv import load_dotenv from binance.client import Client from binance.exceptions import BinanceAPIException logging.basicConfig(level=logging.INFO, format='%(asctime)s | %(message)s') logger = logging.getLogger() load_dotenv() 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) SYMBOLS = ['BTCUSDT', 'ETHUSDT', 'BNBUSDT', 'XRPUSDT', 'SOLUSDT'] TRACKED_COINS = ['BTC', 'ETH', 'BNB', 'XRP', 'SOL'] MIN_TRADE_USDT = 12.00 MAX_POSITION_PCT = 0.07 TAKE_PROFIT_PCT = 0.015 STOP_LOSS_PCT = -0.008 CYCLE_SEC = 60 class TradingBotV04: def __init__(self): self.client = Client(API_KEY, API_SECRET) self.price_history = {sym: [] for sym in SYMBOLS} self.active_trades = {} self.portfolio_value = 0 self.max_trade_usdt = 0 # TRADE RECOVERY: Recover orphaned trades from holdings try: account = self.client.get_account() for b in account['balances']: asset = b['asset'] free = float(b['free']) if asset in TRACKED_COINS and free > 0.0001: symbol = asset + 'USDT' try: price = self.get_current_price(symbol) if price: self.active_trades[symbol] = { 'entry_price': price, 'qty': free, 'entry_time': datetime.now().isoformat() } logger.info(f"[RECOVERED] {symbol} {free} @ {price}") except: pass except Exception as e: logger.warning(f"Recovery failed: {e}") logger.info("[v0.4 INIT] Bot | Dynamic Sizing (Min 12 + 7%)") def get_fresh_balance(self): try: account = self.client.get_account() 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 = portfolio_value * MAX_POSITION_PCT logger.info(f"[v0.4] USDT={usdt_available:.2f} | Portfolio={portfolio_value:.2f} | Max={self.max_trade_usdt:.2f}") return usdt_available, portfolio_value except: return 0, 0 def get_current_price(self, symbol): try: ticker = self.client.get_ticker(symbol=symbol) return float(ticker['lastPrice']) except: return None def calculate_valid_quantity(self, symbol, usdt_amount): try: price = self.get_current_price(symbol) if not price or price <= 0: return 0 info = self.client.get_symbol_info(symbol) if not info: return 0 step_size = None for f in info.get('filters', []): if f['filterType'] == 'LOT_SIZE': step_size = float(f['stepSize']) break if not step_size or step_size <= 0: return 0 qty = usdt_amount / price qty = int(qty / step_size) * step_size if qty * price < 5.0: return 0 return qty except: return 0 def is_local_minimum(self, symbol): if len(self.price_history[symbol]) < 5: return False 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"[SIGNAL] Local min: {symbol} @ {current}") return is_min def place_buy_order(self, symbol, usdt_amount): try: qty = self.calculate_valid_quantity(symbol, usdt_amount) if qty <= 0: return None price = self.get_current_price(symbol) if not price: return None order = self.client.order_market_buy(symbol=symbol, quantity=qty) self.active_trades[symbol] = { 'entry_price': price, 'qty': qty, 'order_id': order.get('orderId'), 'entry_time': datetime.now().isoformat() } pos_pct = (qty * price / self.portfolio_value * 100) if self.portfolio_value > 0 else 0 logger.info(f"[BUY] {symbol} {qty} @ {price} | Pos: {pos_pct:.1}% [v0.4 HYBRID]") return order except: return None def check_and_close_positions(self): 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} | +{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} | {pnl_pct:.2f}%") try: self.client.order_market_sell(symbol=symbol, quantity=qty) del self.active_trades[symbol] except: pass except: pass def run_cycle(self): logger.info("="*70) usdt_free, portfolio_val = self.get_fresh_balance() if usdt_free < MIN_TRADE_USDT: logger.warning(f"Low capital: {usdt_free:.2f} < {MIN_TRADE_USDT}") logger.info("="*70) return self.check_and_close_positions() for symbol in SYMBOLS: price = self.get_current_price(symbol) if price: self.price_history[symbol].append(price) if len(self.price_history[symbol]) > 20: self.price_history[symbol].pop(0) best_signal = None for symbol in SYMBOLS: if symbol not in self.active_trades and self.is_local_minimum(symbol): best_signal = symbol break if best_signal and usdt_free >= MIN_TRADE_USDT: trade_amount = min(max(MIN_TRADE_USDT, self.max_trade_usdt), usdt_free * 0.5) self.place_buy_order(best_signal, trade_amount) logger.info(f"[CYCLE-END] Trades: {len(self.active_trades)} | USDT: {usdt_free:.2f} | Portfolio: {portfolio_val:.2f}") try: 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-hybrid' }, f) os.replace(temp, '/home/marc/bot-deploy/active_trades.json') except Exception as e: logger.warning(f"Save failed: {e}") logger.info("="*70) if __name__ == '__main__': import sys bot = TradingBotV04() if len(sys.argv) > 1 and sys.argv[1] == '--once': bot.run_cycle() else: logger.info("[v0.4 START] Bot cycle loop...") while True: try: bot.run_cycle() except Exception as e: logger.error(f"Error: {e}") time.sleep(CYCLE_SEC)