BrainDock/src/main_ml.py.backup_sweep

591 lines
22 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
Trading Bot V0.2 — Adaptive Strategy Learning
Implementiert: SL, TP, Daily Limit, R:R Ratio
FIXED: Binance API method (order_take_profit → create_order)
FIXED: PRICE_FILTER für SL Orders durch Tick-Rounding
FIXED: Quantity rounding mit Decimal (no floating point errors)
FIXED: Quantity string formatting für Binance
NEW: Startup Message + 3h Performance Reports via Telegram
"""
import os, asyncio, logging, random, json, time, math, requests
from decimal import Decimal, ROUND_DOWN
from binance.client import Client
from binance.exceptions import BinanceAPIException
from datetime import datetime, timedelta
# Logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# Load env
env = {}
with open('/home/marc/bot-deploy/.env') as f:
for line in f:
k,_,v = line.partition('=')
env[k.strip()] = v.strip()
class TradingBot:
def __init__(self):
self.client = Client(env.get('BINANCE_API_KEY_LIVE'), env.get('BINANCE_API_SECRET_LIVE'))
self.PAIRS = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT']
self.SIGNAL_THRESHOLD = 7.5 # 7-8% range (midpoint 7.5%) # 5% random signal
self.INVESTMENT_PERCENT = 50 # 50% (single position for liquidity) (single position)
self.INVESTMENT_PERCENT_HIGH = 55 # 55% when confidence > 85% > 85%
self.CONFIDENCE_THRESHOLD = 85 # Min confidence for high investment # 35% per trade (5 parallel = 90% max, 10% buffer)
self.NOTIONAL_MIN = 5.0 # Override Binance minimum to $3
self.STOP_LOSS_PERCENT = 1.8 # -2.5%
self.TAKE_PROFIT_PERCENT = 2.8 # +3%
self.DAILY_LOSS_LIMIT = -5
# Trailing Stop
self.TRAILING_STOP_ENTRY = 1.5 # Activate trailing stop at +1.5%
self.TRAILING_STOP_DISTANCE = 0.6 # 0.6% distance
# Position & Trade Limits
self.MAX_OPEN_POSITIONS = 1 # Single position for max liquidity # Max concurrent trades
self.MAX_CONSECUTIVE_LOSSES = 3 # Stop after 3 losses
self.CONSECUTIVE_LOSS_COOLDOWN = 30 * 60 # 30 minutes in seconds
self.MAX_TRADES_PER_DAY = 15
self.MIN_WIN_PROBABILITY = 75 # Min expected win %
# Tracking
self.consecutive_losses = 0
self.last_loss_time = None
self.trades_today = 0
self.last_trade_reset = None # -5% max
# Profit tracking
self.entry_price_history = {} # symbol -> entry price
self.closed_trades = [] # list of {symbol, entry, exit, profit_pct, profit_usdt}
self.session_start_balance = None
self.active_trades = {}
self.daily_pnl = 0
self.paused = False
# ADAPTIVE TRACKING (Option 2: Win Rate based Strategy)
self.total_trades = 0
self.total_wins = 0
self.total_losses = 0
self.last_win_rate = 50.0 # Start neutral
self.strategy_version = 1
self.start_time = datetime.now()
self.trades_today = 0
self.wins_today = 0
self.losses_today = 0
# Precision cache
self.pair_precision = {}
self._load_pair_precision()
# Telegram
self.telegram_token = env.get('TELEGRAM_BOT_TOKEN')
self.telegram_chat_id = env.get('TELEGRAM_CHAT_ID')
logger.info(f"✅ Bot initialized with Risk Management (SL {self.STOP_LOSS_PERCENT}%, TP {self.TAKE_PROFIT_PERCENT}%, Daily Limit {-self.DAILY_LOSS_LIMIT}%, Max Pos: {self.MAX_OPEN_POSITIONS})")
# Send startup message
self._send_startup_message()
def _send_telegram(self, message):
"""Send message to Telegram"""
try:
if not self.telegram_token or not self.telegram_chat_id:
logger.warning("Telegram not configured")
return False
url = f"https://api.telegram.org/bot{self.telegram_token}/sendMessage"
data = {
'chat_id': self.telegram_chat_id,
'text': message,
'parse_mode': 'Markdown'
}
response = requests.post(url, data=data, timeout=5)
return response.status_code == 200
except Exception as e:
logger.error(f"Telegram Error: {e}")
return False
def _send_startup_message(self):
"""Send startup message with current strategy"""
message = """🤖 **TRADING BOT V0.2 — STARTED!**
⚙️ **AKTUELLE STRATEGIE:**
**Entry:**
• Signal: 5% Random (5 sec cycle)
• Investment: 18% USDT per trade ← FIXED!
• Pairs: BTC, ETH, SOL, BNB, XRP
• Max Parallel: 5 trades (5×18% = 90% max)
**Exit:**
• Take Profit: +3.0%
• Stop Loss: -2.5%
• Risk/Reward: 1:1.2
**Risk Management:**
• Daily Loss Limit: -5%
• Position Size Cap: 18%
• Buffer Reserve: 10% USDT
• SL Auto-Place: Ja (korrekt gerundet)
**Status:** 🟢 LIVE
• Time: """ + datetime.now().strftime('%Y-%m-%d %H:%M UTC') + """
• Capital Ready: 100% USDT
---
Reports: Alle 3h via Telegram 📊"""
self._send_telegram(message)
logger.info("📱 Startup message sent to Telegram")
def _load_pair_precision(self):
"""Load Binance precision rules for each pair"""
for pair in self.PAIRS:
try:
info = self.client.get_symbol_info(symbol=pair)
for f in info['filters']:
if f['filterType'] == 'PRICE_FILTER':
tick = float(f['tickSize'])
self.pair_precision[pair] = {
'tick': tick,
'decimals': self._get_decimals(tick)
}
if f['filterType'] == 'LOT_SIZE':
step = float(f['stepSize'])
if pair not in self.pair_precision:
self.pair_precision[pair] = {}
self.pair_precision[pair]['step'] = step
self.pair_precision[pair]['step_decimals'] = self._get_decimals(step)
if f['filterType'] == 'NOTIONAL':
min_notional = float(f['minNotional'])
if pair not in self.pair_precision:
self.pair_precision[pair] = {}
self.pair_precision[pair]['min_notional'] = min_notional
except Exception as e:
logger.error(f"Precision load {pair}: {e}")
def _get_decimals(self, tick):
"""Get decimal places from tick size"""
s = str(tick)
if 'e' in s:
return int(s.split('e-')[1]) if 'e-' in s else 0
return len(s.split('.')[1]) if '.' in s else 0
def _round_to_tick(self, price, pair):
"""Round price to Binance tick size using Decimal"""
tick = self.pair_precision.get(pair, {}).get('tick', 0.01)
price_decimal = Decimal(str(price))
tick_decimal = Decimal(str(tick))
rounded = (price_decimal / tick_decimal).quantize(Decimal('1'), rounding=ROUND_DOWN) * tick_decimal
return float(rounded)
def _round_quantity(self, qty, pair):
"""Round quantity to Binance step size using Decimal - NO PRECISION LOSS"""
step = self.pair_precision.get(pair, {}).get('step', 0.00001)
step_decimals = self.pair_precision.get(pair, {}).get('step_decimals', 5)
qty_decimal = Decimal(str(qty))
step_decimal = Decimal(str(step))
# Round down (safe side)
rounded = (qty_decimal / step_decimal).quantize(Decimal('1'), rounding=ROUND_DOWN) * step_decimal
# Format as string with exactly the right decimals
format_str = f"0.{'':<{step_decimals}}"
if step_decimals == 0:
return int(rounded)
return float(rounded)
async def signal_buy(self, pair):
"""Generate random 5% buy signal"""
rand = random.randint(1, 100)
return rand <= self.SIGNAL_THRESHOLD
async def place_buy_order(self, pair):
"""Place market buy order"""
try:
# Get current price
ticker = self.client.get_ticker(symbol=pair)
entry_price = float(ticker['lastPrice'])
# Calculate quantity
account = self.client.get_account()
usdt_balance = next((b['free'] for b in account['balances'] if b['asset'] == 'USDT'), 0)
usdt = float(usdt_balance) * (self.INVESTMENT_PERCENT / 100)
qty = usdt / entry_price
# ROUND QUANTITY TO STEP SIZE (CRITICAL FIX WITH DECIMAL!)
qty = self._round_quantity(qty, pair)
# Check if qty is valid (not zero after rounding)
if qty <= 0:
logger.warning(f"Quantity too small for {pair}: {qty}")
return False
# VALIDATE NOTIONAL (order_value must be >= 3.0 MINIMUM)
order_value = qty * entry_price
NOTIONAL_MIN = 5.0 # Minimum $3
if order_value < NOTIONAL_MIN:
logger.warning(f"Order value too small {pair}: ${order_value:.2f} < ${NOTIONAL_MIN:.2f} (qty={qty}, price={entry_price})")
return False
logger.info(f"✅ NOTIONAL Check Passed: {pair} ${order_value:.2f} >= ${NOTIONAL_MIN:.2f}")
# Place market buy
order = self.client.order_market_buy(symbol=pair, quantity=qty)
logger.info(f"🟢 BUY: {pair} x{qty} @ ${entry_price:.2f} (value: ${order_value:.2f})")
# Store trade
self.active_trades[pair] = {
'entry': entry_price,
'qty': qty,
'time': datetime.now()
}
# Place SL order (FIXED WITH CORRECT API METHOD)
await self.place_stop_loss(pair, entry_price, qty)
self.trades_today += 1
return True
except Exception as e:
logger.error(f"Buy Error {pair}: {e}")
return False
async def place_stop_loss(self, pair, entry_price, qty):
"""Place stop loss order with correct precision & API method"""
try:
# Calculate SL price with {self.STOP_LOSS_PERCENT}% loss
sl_price = entry_price * (1 - self.STOP_LOSS_PERCENT / 100)
# ROUND TO TICK SIZE (CRITICAL FIX!)
sl_price = self._round_to_tick(sl_price, pair)
# ROUND QUANTITY TO STEP SIZE (WITH DECIMAL!)
qty_rounded = self._round_quantity(qty, pair)
# Place SL order using create_order (correct Binance API method)
order = self.client.create_order(
symbol=pair,
side='SELL',
type='STOP_LOSS_LIMIT',
timeInForce='GTC',
quantity=qty_rounded,
stopPrice=sl_price,
price=sl_price # For STOP_LOSS_LIMIT, need price = stopPrice
)
logger.info(f"🛡️ SL: {pair} x{qty_rounded} @ ${sl_price:.4f} (-{self.STOP_LOSS_PERCENT}%)")
except BinanceAPIException as e:
logger.error(f"SL Error {pair}: {e}")
async def monitor_positions(self):
"""Monitor open positions for TP/SL"""
try:
account = self.client.get_account()
for pair in list(self.active_trades.keys()):
ticker = self.client.get_ticker(symbol=pair)
current = float(ticker['lastPrice'])
entry = self.active_trades[pair]['entry']
gain_percent = ((current - entry) / entry) * 100
# Check TP
if gain_percent >= self.TAKE_PROFIT_PERCENT:
await self.close_position(pair, 'TP', current)
# Check SL (secondary check)
elif gain_percent <= -self.STOP_LOSS_PERCENT:
await self.close_position(pair, 'SL', current)
except Exception as e:
logger.error(f"Monitor Error: {e}")
async def close_position(self, pair, reason, current_price):
"""Close position"""
if pair not in self.active_trades:
return
qty = self.active_trades[pair]['qty']
entry = self.active_trades[pair]['entry']
pnl = (current_price - entry) * qty
logger.info(f"📊 {reason}: {pair} closed @ ${current_price:.2f}, PnL: ${pnl:.2f}")
del self.active_trades[pair]
self.daily_pnl += pnl
if pnl > 0:
self.wins_today += 1
else:
self.losses_today += 1
# Check daily loss limit
if self.daily_pnl <= self.DAILY_LOSS_LIMIT:
logger.warning(f"⚠️ DAILY LOSS LIMIT REACHED: ${self.daily_pnl:.2f}")
self.paused = True
def get_performance_report(self):
"""Get current performance metrics"""
try:
account = self.client.get_account()
balance = {}
for asset_data in account['balances']:
asset = asset_data['asset']
free = float(asset_data['free'])
locked = float(asset_data['locked'])
total = free + locked
if total > 0.00001:
balance[asset] = {
'free': free,
'locked': locked,
'total': total
}
# Get prices
prices = {}
for pair in self.PAIRS:
try:
ticker = self.client.get_ticker(symbol=pair)
asset = pair.replace('USDT', '')
prices[asset] = float(ticker['lastPrice'])
except:
pass
prices['USDT'] = 1.0
# Calculate portfolio
portfolio = 0
tracked = ['BTC', 'ETH', 'SOL', 'BNB', 'XRP', 'USDT']
for asset in tracked:
if asset in balance:
portfolio += balance[asset]['total'] * prices.get(asset, 0)
return {
'portfolio': round(portfolio, 2),
'usdt_free': balance.get('USDT', {}).get('free', 0),
'daily_pnl': self.daily_pnl,
'trades_today': self.trades_today,
'wins': self.wins_today,
'losses': self.losses_today,
'active_trades': len(self.active_trades),
'paused': self.paused
}
except Exception as e:
logger.error(f"Performance Report Error: {e}")
return None
def send_performance_report(self):
"""Send 3h performance report via Telegram"""
report = self.get_performance_report()
if not report:
return
win_rate = 0
if report['trades_today'] > 0:
win_rate = (report['wins'] / report['trades_today']) * 100
status = "🟢 RUNNING" if not report['paused'] else "⏸️ PAUSED"
message = f"""📊 **3H PERFORMANCE REPORT**
**Portfolio Status:**
• Total: ${report['portfolio']:.2f}
• USDT Free: ${report['usdt_free']:.2f}
• Status: {status}
**Today's Trading:**
• Trades Executed: {report['trades_today']}
• Wins: {report['wins']}
• Losses: {report['losses']}
• Win Rate: {win_rate:.1f}%
**P&L:**
• Daily P&L: ${report['daily_pnl']:.2f}
• Open Positions: {report['active_trades']}
**Risk Status:**
• Daily Loss Limit: -5%
• Current Daily Loss: ${report['daily_pnl']:.2f}
• Pause Active: {'Yes ⏸️' if report['paused'] else 'No ✅'}
---
Time: {datetime.now().strftime('%Y-%m-%d %H:%M UTC')}
Bot: V0.2 Adaptive"""
self._send_telegram(message)
logger.info("📱 Performance report sent to Telegram")
async def run_cycle(self):
"""Main trading cycle"""
last_report_hour = None
while True:
try:
# Check if it's time for 3h report
current_hour = datetime.now().hour
if current_hour % 3 == 0 and last_report_hour != current_hour:
self.send_performance_report()
last_report_hour = current_hour
# Check daily loss limit pause
if self.paused:
logger.info("⏸️ Bot PAUSED (daily loss limit reached)")
await asyncio.sleep(60)
continue
# Signal generation
for pair in self.PAIRS:
if pair not in self.active_trades and await self.signal_buy(pair):
await self.place_buy_order(pair)
# Monitor positions
await self.monitor_positions()
await asyncio.sleep(5)
except Exception as e:
logger.error(f"Cycle Error: {e}")
await asyncio.sleep(5)
async def main():
bot = TradingBot()
await bot.run_cycle()
if __name__ == '__main__':
asyncio.run(main())
def get_signal_confidence(self):
"""Calculate confidence level for current signal (0-100%)"""
# This can be enhanced with actual ML model
# For now: random 30-95%
import random
return random.uniform(30, 95)
def get_investment_percent(self, confidence):
"""Select investment % based on confidence"""
return self.INVESTMENT_PERCENT_HIGH if confidence > self.CONFIDENCE_THRESHOLD else self.INVESTMENT_PERCENT
def check_consecutive_loss_cooldown(self):
"""Check if bot is in cooldown after 3 consecutive losses"""
if self.consecutive_losses >= self.MAX_CONSECUTIVE_LOSSES:
if self.last_loss_time is None:
return False # First loss, no cooldown
time_elapsed = time.time() - self.last_loss_time
if time_elapsed < self.CONSECUTIVE_LOSS_COOLDOWN:
logger.warning(f"🚫 Cooldown active: {int(self.CONSECUTIVE_LOSS_COOLDOWN - time_elapsed)}s remaining")
return False
else:
# Cooldown expired, reset counter
self.consecutive_losses = 0
logger.info("✅ Cooldown expired, consecutive loss counter reset")
return True
return True
def check_volatility(self, pair):
"""Check market volatility (simplified)"""
try:
ticker = self.client.get_symbol_ticker(symbol=pair)
current_price = float(ticker['price'])
# Get 1h candle for volatility estimate
candles = self.client.get_klines(symbol=pair, interval='1h', limit=5)
high_prices = [float(c[2]) for c in candles]
low_prices = [float(c[3]) for c in candles]
volatility = (max(high_prices) - min(low_prices)) / min(low_prices) * 100
# Flag as extreme if > 5% 1h volatility
if volatility > 5:
logger.warning(f"⚠️ High volatility {pair}: {volatility:.2f}% (skipping trade)")
return False
return True
except:
return True # If check fails, allow trade
def check_daily_trade_limit(self):
"""Check if daily trade limit reached"""
import datetime
now = datetime.datetime.now()
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
if self.last_trade_reset is None or self.last_trade_reset < today_start:
self.trades_today = 0
self.last_trade_reset = now
if self.trades_today >= self.MAX_TRADES_PER_DAY:
logger.warning(f"⚠️ Daily limit reached: {self.trades_today}/{self.MAX_TRADES_PER_DAY} trades")
return False
return True
def update_trailing_stop(self, pair, current_price, entry_price):
"""Update trailing stop for an open position"""
if pair not in self.active_trades:
return False
trade_data = self.active_trades[pair]
profit_pct = ((current_price - entry_price) / entry_price) * 100
# Activate trailing stop when profit >= 1.5%
if profit_pct >= self.TRAILING_STOP_ENTRY:
trailing_stop_price = current_price * (1 - self.TRAILING_STOP_DISTANCE / 100)
trade_data['trailing_stop'] = trailing_stop_price
# If price falls below trailing stop, close position
if current_price < trailing_stop_price:
logger.info(f"🛑 Trailing stop triggered {pair}: Sell @ ${current_price:.2f}")
return True
return False
def record_entry(self, pair, price, quantity):
"""Record entry price for profit calculation"""
self.entry_price_history[pair] = {
'price': price,
'qty': quantity,
'value': price * quantity,
'timestamp': time.time()
}
def calculate_unrealized_pnl(self):
"""Calculate unrealized P&L for open positions"""
try:
prices = get_live_prices()
total_unrealized = 0
for pair, entry_data in self.entry_price_history.items():
asset = pair.replace('USDT', '')
current_price = prices.get(asset, 0)
if current_price > 0:
current_value = entry_data['qty'] * current_price
unrealized = current_value - entry_data['value']
total_unrealized += unrealized
return total_unrealized
except:
return 0
def calculate_realized_pnl(self):
"""Sum all closed trades realized P&L"""
return sum(t.get('profit_usdt', 0) for t in self.closed_trades)
def get_total_pnl(self):
"""Total P&L = realized + unrealized"""
return self.calculate_realized_pnl() + self.calculate_unrealized_pnl()