diff --git a/src/main_ml_v04.py b/src/main_ml_v04.py new file mode 100644 index 0000000..97b70a1 --- /dev/null +++ b/src/main_ml_v04.py @@ -0,0 +1,245 @@ +#!/usr/bin/env python3 +'''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 + +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) + +# CONSTANTS - DYNAMIC SIZING +SYMBOLS = ['BTCUSDT', 'ETHUSDT', 'BNBUSDT', 'XRPUSDT', 'SOLUSDT'] +TRACKED_COINS = ['BTC', 'ETH', 'BNB', 'XRP', 'SOL'] +MIN_USDT = 5.00 +MAX_POSITION_PCT = 0.05 +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 + logger.info("[v0.4 INIT] Bot initialized | Dynamic Position Sizing") + + 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 = 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, 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:.2f}") + + 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:.2f} | Position: {pos_pct:.1f}% | [v0.4 DYNAMIC]") + + return order + except BinanceAPIException as e: + logger.error(f"Order failed: {e}") + 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:.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 run_cycle(self): + logger.info("=" * 70) + + 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 + + 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_USDT: + trade_amount = min(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} [v0.4]") + + 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-dynamic' + }, 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] 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)