Bot auto-update: src/main_ml.py,src/main_ml_v02_backup_1783615772.py
This commit is contained in:
parent
82be6ffe94
commit
cba8e4f4d4
890
src/main_ml.py
890
src/main_ml.py
|
|
@ -1,682 +1,246 @@
|
|||
#!/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
|
||||
Trading Bot V0.3 - Strategy Rewrite
|
||||
Deployed: 2026-07-09 18:30 UTC
|
||||
Changes: Fresh balance cache, local min signal, hard TP/SL
|
||||
"""
|
||||
import os, asyncio, logging, random, json, time, math, requests
|
||||
from decimal import Decimal, ROUND_DOWN
|
||||
import os
|
||||
import time
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from dotenv import load_dotenv
|
||||
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()
|
||||
# Setup
|
||||
logging.basicConfig(level=logging.INFO, format='%(levelname)s:%(message)s')
|
||||
logger = logging.getLogger()
|
||||
|
||||
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"""
|
||||
load_dotenv()
|
||||
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'])
|
||||
API_KEY = os.getenv('BINANCE_API_KEY_LIVE')
|
||||
API_SECRET = os.getenv('BINANCE_API_SECRET_LIVE')
|
||||
except:
|
||||
pass
|
||||
prices['USDT'] = 1.0
|
||||
logger.error("Missing API keys")
|
||||
exit(1)
|
||||
|
||||
# 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)
|
||||
# Constants
|
||||
SYMBOLS = ['BTCUSDT', 'ETHUSDT', 'BNBUSDT', 'XRPUSDT', 'SOLUSDT']
|
||||
MIN_USDT = 5.00
|
||||
MAX_TRADE_USDT = 20.00
|
||||
TAKE_PROFIT_PCT = 0.015 # +1.5%
|
||||
STOP_LOSS_PCT = -0.008 # -0.8%
|
||||
CYCLE_SEC = 60
|
||||
|
||||
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}")
|
||||
|
||||
class TradingBotV03:
|
||||
"""Trading Bot with Fresh Cache + Local Min Signals + Hard Risk Management"""
|
||||
|
||||
def __init__(self):
|
||||
self.client = Client(API_KEY, API_SECRET)
|
||||
self.price_history = {sym: [] for sym in SYMBOLS}
|
||||
self.active_trades = {} # {symbol: {'entry_price': float, 'qty': float}}
|
||||
logger.info("Bot V0.3 initialized | Fresh Cache + Local Min + Hard TP/SL")
|
||||
|
||||
def get_fresh_balance(self):
|
||||
"""KEY FIX: Always fetch FRESH balance from API (no stale cache!)"""
|
||||
try:
|
||||
account = self.client.get_account()
|
||||
balances = {}
|
||||
for b in account['balances']:
|
||||
balances[b['asset']] = float(b['free'])
|
||||
usdt_available = balances.get('USDT', 0)
|
||||
logger.info(f"Fresh balance: USDT=${usdt_available:.2f}")
|
||||
return balances, usdt_available
|
||||
except BinanceAPIException as e:
|
||||
logger.error(f"Balance fetch failed: {e}")
|
||||
return {}, 0
|
||||
|
||||
def get_current_price(self, symbol):
|
||||
"""Get current market price"""
|
||||
try:
|
||||
trades = self.client.get_recent_trades(symbol=symbol, limit=1)
|
||||
if trades:
|
||||
return float(trades[0]['price'])
|
||||
return None
|
||||
except:
|
||||
return None
|
||||
|
||||
def swap_coins_to_usdt(self):
|
||||
"""
|
||||
AUTO-SWAP: Konvertiere alle freien (unlocked) Coins → USDT
|
||||
Ignoriert locked Coins (von aktiven Trades)
|
||||
Skip-list: LDBTTC (shitcoin), LDDOGE (shitcoin), USDC (dust)
|
||||
"""
|
||||
skip_coins = ['USDT', 'LDBTTC', 'LDDOGE', 'USDC'] # Never swap these
|
||||
|
||||
def calculate_valid_quantity(self, symbol, usdt_amount):
|
||||
"""Calculate valid order quantity respecting LOT_SIZE"""
|
||||
try:
|
||||
balance = self.client.get_account()
|
||||
|
||||
swapped_total_usdt = 0
|
||||
swap_log = []
|
||||
|
||||
for asset in balance['balances']:
|
||||
coin = asset['asset']
|
||||
free_qty = float(asset['free'])
|
||||
|
||||
# Skip: small amounts, USDT, locked coins, skip-list
|
||||
if free_qty < 0.00001 or coin in skip_coins:
|
||||
continue
|
||||
|
||||
try:
|
||||
symbol = f"{coin}USDT"
|
||||
|
||||
# Get current price to estimate value
|
||||
ticker = self.client.get_symbol_info(symbol)
|
||||
if not ticker:
|
||||
logger.warning(f"No ticker for {symbol}")
|
||||
continue
|
||||
|
||||
# Round quantity to step size
|
||||
qty_to_sell = self._round_quantity(free_qty, symbol)
|
||||
if qty_to_sell < 0.00001:
|
||||
continue
|
||||
|
||||
# MARKET SELL (immediate)
|
||||
order = self.client.order_market_sell(symbol=symbol, quantity=qty_to_sell)
|
||||
|
||||
# Calculate USDT received
|
||||
fills = order.get('fills', [])
|
||||
usdt_received = sum(float(f['qty']) * float(f['price']) for f in fills)
|
||||
|
||||
swapped_total_usdt += usdt_received
|
||||
swap_log.append(f"✅ {coin}: {qty_to_sell:.6f} → ${usdt_received:.2f}")
|
||||
logger.info(f"Sweep: Sold {qty_to_sell} {coin} for ${usdt_received:.2f}")
|
||||
|
||||
except BinanceAPIException as e:
|
||||
logger.warning(f"Sweep {coin}: Binance Error {e.status_code} - {e.message}")
|
||||
swap_log.append(f"❌ {coin}: {e.message}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Sweep {coin}: {e}")
|
||||
swap_log.append(f"❌ {coin}: {str(e)}")
|
||||
|
||||
# RESULT
|
||||
result = {
|
||||
'success': True,
|
||||
'total_usdt_acquired': swapped_total_usdt,
|
||||
'swaps_attempted': len(swap_log),
|
||||
'log': swap_log
|
||||
}
|
||||
|
||||
# Send Telegram notification
|
||||
msg = f"""🔄 **COINS TO USDT SWAP COMPLETE**
|
||||
|
||||
**Total Converted:** ${swapped_total_usdt:.2f} → USDT
|
||||
|
||||
{chr(10).join(swap_log)}
|
||||
|
||||
**New USDT Balance:** ${self.get_usdt_balance():.2f}
|
||||
"""
|
||||
self._send_telegram(msg)
|
||||
|
||||
logger.info(f"Swap complete: ${swapped_total_usdt:.2f} converted")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Swap error: {e}")
|
||||
self._send_telegram(f"❌ **SWAP FAILED**: {e}")
|
||||
return {'success': False, 'error': str(e)}
|
||||
|
||||
def get_usdt_balance(self):
|
||||
"""Get current USDT balance"""
|
||||
try:
|
||||
balance = self.client.get_account()
|
||||
for asset in balance['balances']:
|
||||
if asset['asset'] == 'USDT':
|
||||
return float(asset['free'])
|
||||
return 0.0
|
||||
except:
|
||||
return 0.0
|
||||
|
||||
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:
|
||||
price = self.get_current_price(symbol)
|
||||
if not price:
|
||||
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)
|
||||
info = self.client.get_symbol_info(symbol)
|
||||
if not info:
|
||||
return 0
|
||||
|
||||
def get_total_pnl(self):
|
||||
"""Total P&L = realized + unrealized"""
|
||||
return self.calculate_realized_pnl() + self.calculate_unrealized_pnl()
|
||||
step_size = 0.00001 # default
|
||||
for filt in info.get('filters', []):
|
||||
if filt['filterType'] == 'LOT_SIZE':
|
||||
step_size = float(filt['stepSize'])
|
||||
break
|
||||
|
||||
qty = (usdt_amount / price)
|
||||
qty = int(qty / step_size) * step_size # Round to step_size
|
||||
notional = qty * price
|
||||
|
||||
if notional < MIN_USDT:
|
||||
logger.debug(f"Order too small: {symbol} ${notional:.2f}")
|
||||
return 0
|
||||
|
||||
return qty
|
||||
except Exception as e:
|
||||
logger.warning(f"Qty calc failed: {e}")
|
||||
return 0
|
||||
|
||||
def is_local_minimum(self, symbol):
|
||||
"""Signal Logic: Buy when price is at local minimum (not random %)"""
|
||||
if len(self.price_history[symbol]) < 5:
|
||||
return False
|
||||
|
||||
recent_prices = self.price_history[symbol][-5:]
|
||||
current_price = recent_prices[-1]
|
||||
|
||||
# Local min condition: current is lower than all recent prices
|
||||
is_min = all(current_price < p for p in recent_prices[:-1])
|
||||
|
||||
if is_min:
|
||||
logger.info(f"Local min detected: {symbol} @ ${current_price:.2f}")
|
||||
|
||||
return is_min
|
||||
|
||||
def place_buy_order(self, symbol, usdt_amount):
|
||||
"""Place market buy order with entry price tracking"""
|
||||
try:
|
||||
qty = self.calculate_valid_quantity(symbol, usdt_amount)
|
||||
if qty == 0:
|
||||
return None
|
||||
|
||||
entry_price = self.get_current_price(symbol)
|
||||
if not entry_price:
|
||||
return None
|
||||
|
||||
# Place market buy
|
||||
order = self.client.order_market_buy(symbol=symbol, quantity=qty)
|
||||
|
||||
# Track entry
|
||||
self.active_trades[symbol] = {
|
||||
'entry_price': entry_price,
|
||||
'qty': qty,
|
||||
'order_id': order.get('orderId'),
|
||||
'entry_time': datetime.now()
|
||||
}
|
||||
|
||||
logger.info(f"BUY: {qty} {symbol} @ ${entry_price:.2f} (${qty*entry_price:.2f})")
|
||||
logger.info(f" TP target: +${qty*entry_price*TAKE_PROFIT_PCT:.2f} ({TAKE_PROFIT_PCT*100:.1f}%)")
|
||||
logger.info(f" SL target: -${qty*entry_price*abs(STOP_LOSS_PCT):.2f} ({STOP_LOSS_PCT*100:.1f}%)")
|
||||
|
||||
return order
|
||||
|
||||
except BinanceAPIException as e:
|
||||
logger.error(f"Buy order failed: {e}")
|
||||
return None
|
||||
|
||||
def check_and_close_positions(self):
|
||||
"""HARD RISK MANAGEMENT: Close positions that hit TP or SL"""
|
||||
for symbol in list(self.active_trades.keys()):
|
||||
trade = self.active_trades[symbol]
|
||||
current_price = self.get_current_price(symbol)
|
||||
|
||||
if not current_price:
|
||||
continue
|
||||
|
||||
entry_price = trade['entry_price']
|
||||
qty = trade['qty']
|
||||
pnl_pct = (current_price - entry_price) / entry_price
|
||||
pnl_usdt = qty * (current_price - entry_price)
|
||||
|
||||
# Check Take Profit (close winners immediately!)
|
||||
if pnl_pct >= TAKE_PROFIT_PCT:
|
||||
logger.info(f"TAKE PROFIT: {symbol} +{pnl_pct*100:.2f}% (${pnl_usdt:.2f})")
|
||||
try:
|
||||
self.client.order_market_sell(symbol=symbol, quantity=qty)
|
||||
del self.active_trades[symbol]
|
||||
except Exception as e:
|
||||
logger.error(f"Sell failed: {e}")
|
||||
continue
|
||||
|
||||
# Check Stop Loss (cut losers fast!)
|
||||
if pnl_pct <= STOP_LOSS_PCT:
|
||||
logger.warning(f"STOP LOSS: {symbol} {pnl_pct*100:.2f}% (${pnl_usdt:.2f})")
|
||||
try:
|
||||
self.client.order_market_sell(symbol=symbol, quantity=qty)
|
||||
del self.active_trades[symbol]
|
||||
except Exception as e:
|
||||
logger.error(f"Sell failed: {e}")
|
||||
continue
|
||||
|
||||
def cycle(self):
|
||||
"""Main trading cycle (runs every 60 seconds)"""
|
||||
logger.info("=" * 70)
|
||||
logger.info(f"CYCLE START @ {datetime.now().strftime('%H:%M:%S CET')}")
|
||||
|
||||
# STEP 1: Fresh balance (KEY FIX for cache bug!)
|
||||
balances, usdt_free = self.get_fresh_balance()
|
||||
|
||||
if usdt_free < MIN_USDT:
|
||||
logger.warning(f"Insufficient capital: ${usdt_free:.2f} < ${MIN_USDT}")
|
||||
logger.info("=" * 70)
|
||||
return
|
||||
|
||||
# STEP 2: Check existing positions (TP/SL logic)
|
||||
self.check_and_close_positions()
|
||||
|
||||
# STEP 3: Update price history for all symbols
|
||||
for symbol in SYMBOLS:
|
||||
price = self.get_current_price(symbol)
|
||||
if price:
|
||||
self.price_history[symbol].append(price)
|
||||
# Keep only last 20 prices
|
||||
if len(self.price_history[symbol]) > 20:
|
||||
self.price_history[symbol].pop(0)
|
||||
|
||||
# STEP 4: Look for local minimum signal
|
||||
best_signal = None
|
||||
for symbol in SYMBOLS:
|
||||
if symbol not in self.active_trades and self.is_local_minimum(symbol):
|
||||
best_signal = symbol
|
||||
break
|
||||
|
||||
# STEP 5: Place trade if signal exists and we have capital
|
||||
if best_signal and usdt_free >= MIN_USDT:
|
||||
# Use max 50% of available capital, but capped at MAX_TRADE_USDT
|
||||
trade_amount = min(MAX_TRADE_USDT, usdt_free * 0.5)
|
||||
self.place_buy_order(best_signal, trade_amount)
|
||||
|
||||
logger.info(f"CYCLE END | Active trades: {len(self.active_trades)} | Free USDT: ${usdt_free:.2f}")
|
||||
logger.info("=" * 70)
|
||||
|
||||
def run(self):
|
||||
"""Infinite trading loop"""
|
||||
logger.info("=" * 70)
|
||||
logger.info("TRADING BOT V0.3 STARTED")
|
||||
logger.info(f"Symbols: {SYMBOLS}")
|
||||
logger.info(f"Strategy: Local Min Signals | Risk: TP=+{TAKE_PROFIT_PCT*100:.1f}% / SL={STOP_LOSS_PCT*100:.1f}%")
|
||||
logger.info(f"Position size: Max ${MAX_TRADE_USDT}/trade (${usdt_free*0.5} = 50% avail)")
|
||||
logger.info(f"KEY FIX: Fresh balance fetched EVERY cycle (no stale cache!)")
|
||||
logger.info("=" * 70)
|
||||
|
||||
try:
|
||||
while True:
|
||||
self.cycle()
|
||||
time.sleep(CYCLE_SEC)
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Bot stopped by user")
|
||||
except Exception as e:
|
||||
logger.error(f"CRITICAL ERROR: {e}")
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
bot = TradingBotV03()
|
||||
bot.run()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,682 @@
|
|||
#!/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 swap_coins_to_usdt(self):
|
||||
"""
|
||||
AUTO-SWAP: Konvertiere alle freien (unlocked) Coins → USDT
|
||||
Ignoriert locked Coins (von aktiven Trades)
|
||||
Skip-list: LDBTTC (shitcoin), LDDOGE (shitcoin), USDC (dust)
|
||||
"""
|
||||
skip_coins = ['USDT', 'LDBTTC', 'LDDOGE', 'USDC'] # Never swap these
|
||||
|
||||
try:
|
||||
balance = self.client.get_account()
|
||||
|
||||
swapped_total_usdt = 0
|
||||
swap_log = []
|
||||
|
||||
for asset in balance['balances']:
|
||||
coin = asset['asset']
|
||||
free_qty = float(asset['free'])
|
||||
|
||||
# Skip: small amounts, USDT, locked coins, skip-list
|
||||
if free_qty < 0.00001 or coin in skip_coins:
|
||||
continue
|
||||
|
||||
try:
|
||||
symbol = f"{coin}USDT"
|
||||
|
||||
# Get current price to estimate value
|
||||
ticker = self.client.get_symbol_info(symbol)
|
||||
if not ticker:
|
||||
logger.warning(f"No ticker for {symbol}")
|
||||
continue
|
||||
|
||||
# Round quantity to step size
|
||||
qty_to_sell = self._round_quantity(free_qty, symbol)
|
||||
if qty_to_sell < 0.00001:
|
||||
continue
|
||||
|
||||
# MARKET SELL (immediate)
|
||||
order = self.client.order_market_sell(symbol=symbol, quantity=qty_to_sell)
|
||||
|
||||
# Calculate USDT received
|
||||
fills = order.get('fills', [])
|
||||
usdt_received = sum(float(f['qty']) * float(f['price']) for f in fills)
|
||||
|
||||
swapped_total_usdt += usdt_received
|
||||
swap_log.append(f"✅ {coin}: {qty_to_sell:.6f} → ${usdt_received:.2f}")
|
||||
logger.info(f"Sweep: Sold {qty_to_sell} {coin} for ${usdt_received:.2f}")
|
||||
|
||||
except BinanceAPIException as e:
|
||||
logger.warning(f"Sweep {coin}: Binance Error {e.status_code} - {e.message}")
|
||||
swap_log.append(f"❌ {coin}: {e.message}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Sweep {coin}: {e}")
|
||||
swap_log.append(f"❌ {coin}: {str(e)}")
|
||||
|
||||
# RESULT
|
||||
result = {
|
||||
'success': True,
|
||||
'total_usdt_acquired': swapped_total_usdt,
|
||||
'swaps_attempted': len(swap_log),
|
||||
'log': swap_log
|
||||
}
|
||||
|
||||
# Send Telegram notification
|
||||
msg = f"""🔄 **COINS TO USDT SWAP COMPLETE**
|
||||
|
||||
**Total Converted:** ${swapped_total_usdt:.2f} → USDT
|
||||
|
||||
{chr(10).join(swap_log)}
|
||||
|
||||
**New USDT Balance:** ${self.get_usdt_balance():.2f}
|
||||
"""
|
||||
self._send_telegram(msg)
|
||||
|
||||
logger.info(f"Swap complete: ${swapped_total_usdt:.2f} converted")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Swap error: {e}")
|
||||
self._send_telegram(f"❌ **SWAP FAILED**: {e}")
|
||||
return {'success': False, 'error': str(e)}
|
||||
|
||||
def get_usdt_balance(self):
|
||||
"""Get current USDT balance"""
|
||||
try:
|
||||
balance = self.client.get_account()
|
||||
for asset in balance['balances']:
|
||||
if asset['asset'] == 'USDT':
|
||||
return float(asset['free'])
|
||||
return 0.0
|
||||
except:
|
||||
return 0.0
|
||||
|
||||
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()
|
||||
|
||||
Loading…
Reference in New Issue