#!/usr/bin/env python3 """Trading Bot v0.6 - Contrarian Buy/Sell (Mean Reversion) Strategy""" import os, json, time, logging, sqlite3 from datetime import datetime, timedelta from dotenv import load_dotenv from binance.client import Client 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 # CONTRARIAN THRESHOLDS CONTRARIAN_BUY_THRESHOLD = -2.0 # Buy when market DOWN 2%+ CONTRARIAN_SELL_THRESHOLD = +2.0 # Sell when market UP 2%+ LOOKBACK_HOURS = 24 # Compare last 24h return class TradingBotV06: def __init__(self): self.client = Client(API_KEY, API_SECRET) self.price_history = {sym: [] for sym in SYMBOLS} self.daily_opens = {} # Store 24h ago prices self.active_trades = {} self.portfolio_value = 0 self.max_trade_usdt = 0 # TRADE RECOVERY 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.6 INIT] Contrarian Buy/Sell (Mean Reversion) Strategy") def calculate_market_return(self): """Calculate 24h market-wide return (Average of all symbols)""" returns = [] for symbol in SYMBOLS: if len(self.price_history[symbol]) < 2: continue current = self.price_history[symbol][-1] # Get price from ~24h ago (or earliest if less than 24h data) reference_idx = max(0, len(self.price_history[symbol]) - 1440) # 1440 = 24h * 60min reference = self.price_history[symbol][reference_idx] if reference > 0: ret = ((current - reference) / reference) * 100 returns.append(ret) if returns: avg_return = sum(returns) / len(returns) return avg_return return 0.0 def is_contrarian_buy_signal(self, symbol): """Buy when MARKET DOWN 2%+ (Mean Reversion: expect bounce)""" market_return = self.calculate_market_return() buy_signal = market_return < CONTRARIAN_BUY_THRESHOLD if buy_signal: logger.info(f"[SIGNAL-CONTRARIAN-BUY] Market DOWN {market_return:.2f}% (Threshold: {CONTRARIAN_BUY_THRESHOLD}%)") return buy_signal def is_contrarian_sell_signal(self, symbol): """Sell when MARKET UP 2%+ (Take profits on rally)""" market_return = self.calculate_market_return() sell_signal = market_return > CONTRARIAN_SELL_THRESHOLD if sell_signal: logger.info(f"[SIGNAL-CONTRARIAN-SELL] Market UP {market_return:.2f}% (Threshold: {CONTRARIAN_SELL_THRESHOLD}%)") return sell_signal 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.6] 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 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() } logger.info(f"[BUY-v0.6] {symbol} {qty} @ {price} (CONTRARIAN: Market DOWN)") return order except: return None def place_sell_order(self, symbol): try: if symbol not in self.active_trades: return None qty = self.active_trades[symbol]['qty'] order = self.client.order_market_sell(symbol=symbol, quantity=qty) price = self.get_current_price(symbol) entry = self.active_trades[symbol]['entry_price'] pnl = ((price - entry) / entry) * 100 logger.info(f"[SELL-v0.6] {symbol} {qty} @ {price} (CONTRARIAN: Market UP, P&L: {pnl:+.2f}%)") del self.active_trades[symbol] 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 # TP Hit if pnl_pct >= TAKE_PROFIT_PCT * 100: logger.info(f"[SELL-TP] {symbol} +{pnl_pct:.2f}%") try: self.client.order_market_sell(symbol=symbol, quantity=qty) del self.active_trades[symbol] except: pass # SL Hit elif pnl_pct <= STOP_LOSS_PCT * 100: logger.info(f"[SELL-SL] {symbol} {pnl_pct:.2f}%") try: self.client.order_market_sell(symbol=symbol, quantity=qty) del self.active_trades[symbol] except: pass except: pass def save_pnl_to_db(self, portfolio_val, usdt_free): try: conn = sqlite3.connect('/home/marc/bot-deploy/pnl_charts.db') baseline = conn.execute('SELECT pv FROM history ORDER BY ts ASC LIMIT 1').fetchone() baseline_pv = baseline[0] if baseline else portfolio_val pu = portfolio_val - baseline_pv pp = (pu / baseline_pv * 100) if baseline_pv > 0 else 0 conn.execute('INSERT INTO history VALUES (?, ?, ?, ?, ?, ?)', (int(datetime.now().timestamp()), portfolio_val, pu, pp, usdt_free, len(self.active_trades))) conn.commit() conn.close() logger.info(f"[DB-LOG] PV={portfolio_val:.2f}, P&L={pp:.2f}%") except Exception as e: logger.warning(f"DB log failed: {e}") 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}") logger.info("="*70) return # Update price history for symbol in SYMBOLS: price = self.get_current_price(symbol) if price: self.price_history[symbol].append(price) if len(self.price_history[symbol]) > 1440: # Keep 24h history self.price_history[symbol].pop(0) # Check for Contrarian SELL (Market UP 2%+) if self.is_contrarian_sell_signal(None): # Sell holdings that are profitable for symbol in list(self.active_trades.keys()): if symbol not in self.active_trades: continue current = self.get_current_price(symbol) if not current: continue entry = self.active_trades[symbol]['entry_price'] pnl_pct = ((current - entry) / entry) * 100 # Only sell if we have profit (avoid unnecessary SL hits on rally) if pnl_pct > 0.5: self.place_sell_order(symbol) break # One sell per cycle # Check TP/SL self.check_and_close_positions() # Check for Contrarian BUY (Market DOWN 2%+) buy_signal = self.is_contrarian_buy_signal(None) if buy_signal and usdt_free >= MIN_TRADE_USDT: # Find best coin to buy (the one with biggest loss) worst_coin = None worst_return = 0 for symbol in SYMBOLS: if symbol in self.active_trades: continue # Skip already held if len(self.price_history[symbol]) < 2: continue current = self.price_history[symbol][-1] ref_idx = max(0, len(self.price_history[symbol]) - 1440) reference = self.price_history[symbol][ref_idx] if reference > 0: ret = ((current - reference) / reference) * 100 if ret < worst_return: worst_return = ret worst_coin = symbol if worst_coin: trade_amount = min(max(MIN_TRADE_USDT, self.max_trade_usdt), usdt_free * 0.5) self.place_buy_order(worst_coin, trade_amount) # Save trades 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.6-contrarian-mean-reversion' }, f) os.replace(temp, '/home/marc/bot-deploy/active_trades.json') except: pass # Save P&L self.save_pnl_to_db(portfolio_val, usdt_free) logger.info(f"[CYCLE-END] Trades={len(self.active_trades)} | Portfolio={portfolio_val:.2f} [v0.6]") logger.info("="*70) if __name__ == '__main__': import sys bot = TradingBotV06() if len(sys.argv) > 1 and sys.argv[1] == '--once': bot.run_cycle() else: logger.info("[v0.6 START] Trading Bot with Contrarian Buy/Sell (Mean Reversion)...") while True: try: bot.run_cycle() except Exception as e: logger.error(f"Error: {e}") time.sleep(CYCLE_SEC)