diff --git a/src/main_ml.py b/src/main_ml.py index 8385392..114abe7 100644 --- a/src/main_ml.py +++ b/src/main_ml.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 -"""Trading Bot v0.5.1 - RSI + Bollinger Bands HYBRID (Confidence Filter)""" +"""Trading Bot v0.6 - Contrarian Buy/Sell (Mean Reversion) Strategy""" import os, json, time, logging, sqlite3 -from datetime import datetime +from datetime import datetime, timedelta from dotenv import load_dotenv from binance.client import Client @@ -23,15 +23,17 @@ MAX_POSITION_PCT = 0.07 TAKE_PROFIT_PCT = 0.015 STOP_LOSS_PCT = -0.008 CYCLE_SEC = 60 -BB_PERIOD = 20 -BB_STD_DEV = 2.0 -RSI_PERIOD = 14 -RSI_THRESHOLD = 35 -class TradingBotV051: +# 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 @@ -59,88 +61,52 @@ class TradingBotV051: except Exception as e: logger.warning(f"Recovery failed: {e}") - logger.info("[v0.5.1 INIT] RSI + Bollinger Bands HYBRID (Confidence Filter)") + logger.info("[v0.6 INIT] Contrarian Buy/Sell (Mean Reversion) Strategy") - def calculate_rsi(self, prices): - """Calculate RSI (14-period standard)""" - if len(prices) < RSI_PERIOD + 1: - return None + def calculate_market_return(self): + """Calculate 24h market-wide return (Average of all symbols)""" + returns = [] - recent = prices[-RSI_PERIOD-1:] - deltas = [recent[i+1] - recent[i] for i in range(len(recent)-1)] + 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) - gains = [d if d > 0 else 0 for d in deltas] - losses = [abs(d) if d < 0 else 0 for d in deltas] + if returns: + avg_return = sum(returns) / len(returns) + return avg_return - avg_gain = sum(gains) / RSI_PERIOD - avg_loss = sum(losses) / RSI_PERIOD - - if avg_loss == 0: - return 100.0 if avg_gain > 0 else 0.0 - - rs = avg_gain / avg_loss - rsi = 100 - (100 / (1 + rs)) - - return rsi + return 0.0 - def calculate_bollinger_bands(self, prices): - """Calculate 20-EMA +/- 2*StdDev""" - if len(prices) < BB_PERIOD: - return None, None, None + def is_contrarian_buy_signal(self, symbol): + """Buy when MARKET DOWN 2%+ (Mean Reversion: expect bounce)""" + market_return = self.calculate_market_return() - # EMA-20 - ema = prices[-1] - alpha = 2.0 / (BB_PERIOD + 1) - for price in prices[-BB_PERIOD:]: - ema = (price * alpha) + (ema * (1 - alpha)) + buy_signal = market_return < CONTRARIAN_BUY_THRESHOLD - # StdDev of last 20 prices - recent_prices = prices[-BB_PERIOD:] - mean = sum(recent_prices) / BB_PERIOD - variance = sum((p - mean) ** 2 for p in recent_prices) / BB_PERIOD - std_dev = variance ** 0.5 + if buy_signal: + logger.info(f"[SIGNAL-CONTRARIAN-BUY] Market DOWN {market_return:.2f}% (Threshold: {CONTRARIAN_BUY_THRESHOLD}%)") - upper_band = ema + (BB_STD_DEV * std_dev) - lower_band = ema - (BB_STD_DEV * std_dev) - - return ema, upper_band, lower_band + return buy_signal - def is_hybrid_buy_signal(self, symbol): - """ - HYBRID Signal: Buy ONLY when BOTH conditions met: - 1. Price rebounds from lower Bollinger Band (BB Breakout) - 2. RSI < 35 (Oversolod confirmation) - """ - if len(self.price_history[symbol]) < max(BB_PERIOD + 1, RSI_PERIOD + 1): - return False + def is_contrarian_sell_signal(self, symbol): + """Sell when MARKET UP 2%+ (Take profits on rally)""" + market_return = self.calculate_market_return() - prices = self.price_history[symbol] - ema, upper, lower = self.calculate_bollinger_bands(prices) + sell_signal = market_return > CONTRARIAN_SELL_THRESHOLD - if not ema or not lower: - return False + if sell_signal: + logger.info(f"[SIGNAL-CONTRARIAN-SELL] Market UP {market_return:.2f}% (Threshold: {CONTRARIAN_SELL_THRESHOLD}%)") - current_price = prices[-1] - prev_price = prices[-2] - - # Signal 1: Bollinger Breakout - bb_breakout = (prev_price < lower and current_price > lower) - - # Signal 2: RSI Oversold - rsi = self.calculate_rsi(prices) - rsi_oversold = (rsi is not None and rsi < RSI_THRESHOLD) - - # HYBRID: Both must be true - hybrid_signal = bb_breakout and rsi_oversold - - if hybrid_signal: - logger.info(f"[SIGNAL-HYBRID] {symbol} RSI={rsi:.1f} + BB-Breakout (EMA={ema:.2f}, Lower={lower:.2f})") - elif bb_breakout and not rsi_oversold: - logger.debug(f"[FILTERED] {symbol} BB-Breakout but RSI={rsi:.1f} (need <{RSI_THRESHOLD})") - elif rsi_oversold and not bb_breakout: - logger.debug(f"[FILTERED] {symbol} RSI={rsi:.1f} but no BB-Breakout") - - return hybrid_signal + return sell_signal def get_fresh_balance(self): try: @@ -171,7 +137,7 @@ class TradingBotV051: self.portfolio_value = portfolio_value self.max_trade_usdt = portfolio_value * MAX_POSITION_PCT - logger.info(f"[v0.5.1] USDT={usdt_available:.2f} | Portfolio={portfolio_value:.2f} | Max={self.max_trade_usdt:.2f}") + 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 @@ -231,7 +197,27 @@ class TradingBotV051: 'entry_time': datetime.now().isoformat() } - logger.info(f"[BUY-v0.5.1] {symbol} {qty} @ {price} (RSI+BB HYBRID)") + 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 @@ -247,6 +233,7 @@ class TradingBotV051: 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: @@ -255,6 +242,7 @@ class TradingBotV051: except: pass + # SL Hit elif pnl_pct <= STOP_LOSS_PCT * 100: logger.info(f"[SELL-SL] {symbol} {pnl_pct:.2f}%") try: @@ -293,26 +281,63 @@ class TradingBotV051: logger.info("="*70) return - self.check_and_close_positions() - # 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]) > 100: + if len(self.price_history[symbol]) > 1440: # Keep 24h history self.price_history[symbol].pop(0) - # Find HYBRID signal (RSI + BB both true) - best_signal = None - for symbol in SYMBOLS: - if symbol not in self.active_trades and self.is_hybrid_buy_signal(symbol): - best_signal = symbol - break + # 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 - 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) + # 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: @@ -324,7 +349,7 @@ class TradingBotV051: 'portfolio_value': round(portfolio_val, 2), 'max_trade_usdt': round(self.max_trade_usdt, 2), 'timestamp': datetime.now().isoformat(), - 'version': 'v0.5.1-rsi-bb-hybrid' + 'version': 'v0.6-contrarian-mean-reversion' }, f) os.replace(temp, '/home/marc/bot-deploy/active_trades.json') except: @@ -333,18 +358,18 @@ class TradingBotV051: # 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.5.1]") + 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 = TradingBotV051() + bot = TradingBotV06() if len(sys.argv) > 1 and sys.argv[1] == '--once': bot.run_cycle() else: - logger.info("[v0.5.1 START] Trading Bot with RSI + Bollinger Bands HYBRID signals...") + logger.info("[v0.6 START] Trading Bot with Contrarian Buy/Sell (Mean Reversion)...") while True: try: bot.run_cycle() diff --git a/src/web_dashboard.py b/src/web_dashboard.py index 7091fd3..f897bff 100644 --- a/src/web_dashboard.py +++ b/src/web_dashboard.py @@ -1,336 +1,141 @@ -#!/usr/bin/env python3 -"""Trading Bot Dashboard v0.5.1 (RSI + Bollinger Bands HYBRID) - Auto-load 1-Day chart on page load""" -import sqlite3 -from fastapi import FastAPI -from fastapi.responses import HTMLResponse +"""Trading Bot Dashboard v0.6 (Contrarian Mean Reversion) - Auto-load 1-Day chart on page load""" +import os, json, logging, sqlite3 +from datetime import datetime, timedelta +from flask import Flask, render_template_string, jsonify from binance.client import Client -from datetime import datetime -import json, os, time +from dotenv import load_dotenv -app = FastAPI() +load_dotenv() +API_KEY = os.getenv('BINANCE_API_KEY_LIVE') +API_SECRET = os.getenv('BINANCE_API_SECRET_LIVE') +client = Client(API_KEY, API_SECRET) -env = {} -with open('/home/marc/bot-deploy/.env') as f: - for line in f: - k, _, v = line.partition('=') - env[k.strip()] = v.strip() +app = Flask(__name__) -binance = Client(env.get('BINANCE_API_KEY_LIVE'), env.get('BINANCE_API_SECRET_LIVE')) -DB = '/home/marc/bot-deploy/pnl_charts.db' +@app.route('/') +def dashboard(): + html = """ + + +
+Contrarian Mean Reversion Strategy
| Symbol | Qty | Entry Price | Entry Time |
|---|
P&L Analytics