Bot V5 ENHANCED: Telegram Notifications + Quantity Precision
- NEW: Startup message with strategy summary (sent to Telegram) - NEW: 3h performance reports (automatic every 3 hours) - NEW: Telegram integration for notifications + metrics - FIXED: Quantity rounding to Binance step size - FIXED: All buy orders now precision-safe - NEW: Performance metrics tracked per day - NEW: Win rate + P&L reporting - NEW: Daily PnL tracking + pause at -5% - FEATURE: Auto-reporting every 3 hours - VERSION: V5 ENHANCED PRODUCTION - STATUS: Ready for 100% USDT trading with Telegram alerts
This commit is contained in:
parent
0fd2bdc647
commit
aacbeaf6df
219
src/main_ml.py
219
src/main_ml.py
|
|
@ -1,10 +1,11 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Trading Bot V5 ENHANCED - Risk Management FIXED
|
||||
Implementiert: SL (mit korrekter Precision), TP, Daily Limit, R:R Ratio
|
||||
Trading Bot V5 ENHANCED - Risk Management + Telegram Notifications
|
||||
Implementiert: SL, TP, Daily Limit, R:R Ratio
|
||||
FIXED: PRICE_FILTER für SL Orders durch Tick-Rounding
|
||||
NEW: Startup Message + 3h Performance Reports via Telegram
|
||||
"""
|
||||
import os, asyncio, logging, random, json, time, math
|
||||
import os, asyncio, logging, random, json, time, math, requests
|
||||
from binance.client import Client
|
||||
from binance.exceptions import BinanceAPIException
|
||||
from datetime import datetime, timedelta
|
||||
|
|
@ -34,12 +35,74 @@ class TradingBot:
|
|||
self.active_trades = {}
|
||||
self.daily_pnl = 0
|
||||
self.paused = False
|
||||
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("✅ Bot initialized with Risk Management (SL 2.5%, TP 3%, Daily Limit 5%)")
|
||||
|
||||
# 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 V5 — STARTED!**
|
||||
|
||||
⚙️ **AKTUELLE STRATEGIE:**
|
||||
|
||||
**Entry:**
|
||||
• Signal: 5% Random (5 sec cycle)
|
||||
• Investment: 25% USDT per trade
|
||||
• Pairs: BTC, ETH, SOL, BNB, XRP
|
||||
• Max Parallel: 5 trades
|
||||
|
||||
**Exit:**
|
||||
• Take Profit: +3.0% ✅
|
||||
• Stop Loss: -2.5% ✅
|
||||
• Risk/Reward: 1:1.2
|
||||
|
||||
**Risk Management:**
|
||||
• Daily Loss Limit: -5%
|
||||
• Position Size Cap: 25%
|
||||
• SL Auto-Place: Ja (korrekt gerundet)
|
||||
|
||||
**Status:** 🟢 LIVE
|
||||
• Time: """ + datetime.now().strftime('%Y-%m-%d %H:%M UTC') + """
|
||||
• Capital Ready: ~$135 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"""
|
||||
|
|
@ -63,11 +126,36 @@ class TradingBot:
|
|||
return int(s.split('e-')[1]) if 'e-' in s else 0
|
||||
return len(s.split('.')[1]) if '.' in s else 0
|
||||
|
||||
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
|
||||
except Exception as e:
|
||||
logger.error(f"Precision load {pair}: {e}")
|
||||
|
||||
def _round_to_tick(self, price, pair):
|
||||
"""Round price to Binance tick size"""
|
||||
tick = self.pair_precision.get(pair, {}).get('tick', 0.01)
|
||||
return round(price / tick) * tick
|
||||
|
||||
def _round_quantity(self, qty, pair):
|
||||
"""Round quantity to Binance step size"""
|
||||
step = self.pair_precision.get(pair, {}).get('step', 0.00001)
|
||||
return round(qty / step) * step
|
||||
|
||||
async def signal_buy(self, pair):
|
||||
"""Generate random 5% buy signal"""
|
||||
rand = random.randint(1, 100)
|
||||
|
|
@ -87,9 +175,17 @@ class TradingBot:
|
|||
|
||||
qty = usdt / entry_price
|
||||
|
||||
# ROUND QUANTITY TO STEP SIZE (CRITICAL FIX!)
|
||||
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
|
||||
|
||||
# Place market buy
|
||||
order = self.client.order_market_buy(symbol=pair, quantity=qty)
|
||||
logger.info(f"🟢 BUY: {pair} x{qty:.6f} @ ${entry_price:.2f}")
|
||||
logger.info(f"🟢 BUY: {pair} x{qty:.8f} @ ${entry_price:.2f}")
|
||||
|
||||
# Store trade
|
||||
self.active_trades[pair] = {
|
||||
|
|
@ -101,6 +197,7 @@ class TradingBot:
|
|||
# Place SL order (FIXED WITH ROUNDING)
|
||||
await self.place_stop_loss(pair, entry_price, qty)
|
||||
|
||||
self.trades_today += 1
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
|
|
@ -116,17 +213,20 @@ class TradingBot:
|
|||
# ROUND TO TICK SIZE (CRITICAL FIX!)
|
||||
sl_price = self._round_to_tick(sl_price, pair)
|
||||
|
||||
# ROUND QUANTITY TO STEP SIZE
|
||||
qty_rounded = self._round_quantity(qty, pair)
|
||||
|
||||
# Place SL order
|
||||
order = self.client.order_take_profit(
|
||||
symbol=pair,
|
||||
side='SELL',
|
||||
type='STOP_LOSS',
|
||||
timeInForce='GTC',
|
||||
quantity=qty,
|
||||
quantity=qty_rounded,
|
||||
stopPrice=sl_price,
|
||||
price=sl_price # Binance requires price = stopPrice for STOP_LOSS
|
||||
price=sl_price
|
||||
)
|
||||
logger.info(f"🛡️ SL: {pair} @ ${sl_price:.4f} (-{self.STOP_LOSS_PERCENT}%)")
|
||||
logger.info(f"🛡️ SL: {pair} x{qty_rounded:.8f} @ ${sl_price:.4f} (-{self.STOP_LOSS_PERCENT}%)")
|
||||
|
||||
except BinanceAPIException as e:
|
||||
logger.error(f"SL Error {pair}: {e}")
|
||||
|
|
@ -168,15 +268,120 @@ class TradingBot:
|
|||
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: V5 ENHANCED"""
|
||||
|
||||
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)")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,220 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Trading Bot V5 ENHANCED - Mit kritischen Risk Management Fixes
|
||||
Implementiert: SL, TP Anpassung, Daily Limit, R:R Ratio
|
||||
"""
|
||||
import os, asyncio, logging, random, json, time
|
||||
from datetime import datetime, timedelta
|
||||
from binance.client import Client
|
||||
from binance.exceptions import BinanceAPIException
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Load config
|
||||
env = {}
|
||||
with open('/home/marc/bot-deploy/.env') as f:
|
||||
for line in f:
|
||||
k, _, v = line.partition('=')
|
||||
env[k.strip()] = v.strip()
|
||||
|
||||
class TradingBotV5Enhanced:
|
||||
def __init__(self):
|
||||
self.binance = Client(env.get('BINANCE_API_KEY_LIVE'), env.get('BINANCE_API_SECRET_LIVE'))
|
||||
self.state_file = '/home/marc/bot-deploy/trades.json'
|
||||
self.load_state()
|
||||
|
||||
# NEW: Risk Management Settings
|
||||
self.STOP_LOSS_PERCENT = 2.5 # 2.5% SL (-2.5%)
|
||||
self.TAKE_PROFIT_PERCENT = 3.0 # 3.0% TP (+3%) - was +1%
|
||||
self.DAILY_LOSS_LIMIT = 5.0 # Max -5% daily
|
||||
self.MIN_RISK_REWARD = 1.5 # Min R:R ratio
|
||||
self.MAX_POSITION_PERCENT = 25 # Max 25% per trade
|
||||
|
||||
logger.info("✅ Bot initialized with Risk Management (SL 2.5%, TP 3%, Daily Limit 5%)")
|
||||
|
||||
def load_state(self):
|
||||
if os.path.exists(self.state_file):
|
||||
with open(self.state_file) as f:
|
||||
self.state = json.load(f)
|
||||
else:
|
||||
self.state = {'current': {}, 'completed': [], 'daily_start_balance': 0}
|
||||
|
||||
def save_state(self):
|
||||
with open(self.state_file, 'w') as f:
|
||||
json.dump(self.state, f, indent=2)
|
||||
|
||||
def check_and_place_sl_orders(self, pair, qty, entry_price):
|
||||
"""
|
||||
NEW: Automatically place Stop Loss orders for existing positions
|
||||
SL = Entry - 2.5%
|
||||
"""
|
||||
sl_price = entry_price * (1 - self.STOP_LOSS_PERCENT / 100)
|
||||
|
||||
try:
|
||||
# Check if already has SL order
|
||||
orders = self.binance.get_open_orders(symbol=pair)
|
||||
has_sl = any(o['side'] == 'SELL' and float(o['price']) < entry_price for o in orders)
|
||||
|
||||
if not has_sl:
|
||||
# Place SL order
|
||||
order = self.binance.order_limit_sell(
|
||||
symbol=pair,
|
||||
quantity=qty,
|
||||
price=round(sl_price, 8)
|
||||
)
|
||||
logger.info(f"🛡️ Stop Loss set: {pair} {qty} @ ${sl_price:.4f}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"SL Error {pair}: {e}")
|
||||
|
||||
return False
|
||||
|
||||
def place_buy(self, pair):
|
||||
"""Place market buy with Risk Management checks"""
|
||||
try:
|
||||
# Get balance
|
||||
balance = self.binance.get_account()
|
||||
usdt_free = float([a['free'] for a in balance['balances'] if a['asset'] == 'USDT'][0])
|
||||
|
||||
# NEW: Daily loss check
|
||||
daily_loss = self.calculate_daily_loss()
|
||||
if daily_loss <= -self.DAILY_LOSS_LIMIT:
|
||||
logger.warning(f"⛔ Daily loss limit hit: {daily_loss:.2f}% (limit: -{self.DAILY_LOSS_LIMIT}%)")
|
||||
return None
|
||||
|
||||
# Calculate position size (25% of USDT)
|
||||
qty_usdt = usdt_free * (self.MAX_POSITION_PERCENT / 100)
|
||||
|
||||
if qty_usdt < 10: # Binance minimum
|
||||
return None
|
||||
|
||||
# Get current price
|
||||
ticker = self.binance.get_symbol_info(pair)
|
||||
price = float(self.binance.get_ticker(symbol=pair)['lastPrice'])
|
||||
|
||||
# Calculate quantity with LOT_SIZE filter
|
||||
lot_filter = next(f for f in ticker['filters'] if f['filterType'] == 'LOT_SIZE')
|
||||
step_size = float(lot_filter['stepSize'])
|
||||
qty = float(int(qty_usdt / price / step_size) * step_size)
|
||||
|
||||
if qty < float(lot_filter['minQty']):
|
||||
return None
|
||||
|
||||
# Place market buy
|
||||
order = self.binance.order_market_buy(symbol=pair, quantity=qty)
|
||||
logger.info(f"🟢 BUY: {pair} x{qty:.6f} @ ${price:.4f}")
|
||||
|
||||
# NEW: Auto-place Stop Loss
|
||||
self.check_and_place_sl_orders(pair, qty, price)
|
||||
|
||||
return order
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Buy Error {pair}: {e}")
|
||||
return None
|
||||
|
||||
def check_take_profit(self):
|
||||
"""NEW: Check and close at +3% TP with SL protection"""
|
||||
try:
|
||||
balance = self.binance.get_account()
|
||||
|
||||
for pair in ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT']:
|
||||
ticker = self.binance.get_ticker(symbol=pair)
|
||||
current_price = float(ticker['lastPrice'])
|
||||
|
||||
# Check if we have open trade
|
||||
if pair in self.state['current']:
|
||||
entry_price = self.state['current'][pair]['buy_price']
|
||||
gain_percent = (current_price - entry_price) / entry_price * 100
|
||||
|
||||
# TP at +3%
|
||||
if gain_percent >= self.TAKE_PROFIT_PERCENT:
|
||||
qty = self.state['current'][pair]['qty']
|
||||
try:
|
||||
order = self.binance.order_market_sell(symbol=pair, quantity=qty)
|
||||
profit_usd = (current_price - entry_price) * qty
|
||||
logger.info(f"💰 TP HIT: {pair} +{gain_percent:.2f}% = ${profit_usd:.2f}")
|
||||
|
||||
# Record completion
|
||||
self.state['completed'].append({
|
||||
'pair': pair,
|
||||
'qty': qty,
|
||||
'buy_price': entry_price,
|
||||
'sell_price': current_price,
|
||||
'profit_percent': gain_percent,
|
||||
'profit_usd': profit_usd
|
||||
})
|
||||
del self.state['current'][pair]
|
||||
self.save_state()
|
||||
except Exception as e:
|
||||
logger.error(f"TP sell error {pair}: {e}")
|
||||
|
||||
# SL at -2.5% (auto-cancelled by limit order but check anyway)
|
||||
elif gain_percent <= -self.STOP_LOSS_PERCENT:
|
||||
qty = self.state['current'][pair]['qty']
|
||||
try:
|
||||
order = self.binance.order_market_sell(symbol=pair, quantity=qty)
|
||||
loss_usd = (current_price - entry_price) * qty
|
||||
logger.warning(f"🛑 SL HIT: {pair} {gain_percent:.2f}% = ${loss_usd:.2f}")
|
||||
|
||||
self.state['completed'].append({
|
||||
'pair': pair,
|
||||
'qty': qty,
|
||||
'buy_price': entry_price,
|
||||
'sell_price': current_price,
|
||||
'profit_percent': gain_percent,
|
||||
'profit_usd': loss_usd
|
||||
})
|
||||
del self.state['current'][pair]
|
||||
self.save_state()
|
||||
except Exception as e:
|
||||
logger.error(f"SL sell error {pair}: {e}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"TP check error: {e}")
|
||||
|
||||
def calculate_daily_loss(self):
|
||||
"""Calculate daily loss percentage"""
|
||||
try:
|
||||
if not self.state['completed']:
|
||||
return 0
|
||||
|
||||
today_trades = [t for t in self.state['completed']
|
||||
if datetime.fromisoformat(t.get('timestamp', datetime.now().isoformat())).date() == datetime.now().date()]
|
||||
|
||||
daily_loss = sum(t.get('profit_usd', 0) for t in today_trades)
|
||||
|
||||
balance = self.binance.get_account()
|
||||
portfolio = sum(float(a['free']) for a in balance['balances'])
|
||||
|
||||
loss_percent = (daily_loss / portfolio * 100) if portfolio > 0 else 0
|
||||
return loss_percent
|
||||
except:
|
||||
return 0
|
||||
|
||||
async def run(self):
|
||||
"""Main trading loop"""
|
||||
logger.info("🚀 Trading Bot V5 ENHANCED started (SL+TP+DailyLimit)")
|
||||
|
||||
while True:
|
||||
try:
|
||||
# Check exits first (TP/SL)
|
||||
self.check_take_profit()
|
||||
|
||||
# Generate signal (5% probability)
|
||||
if random.random() < 0.05:
|
||||
pairs = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT']
|
||||
for pair in pairs:
|
||||
if pair not in self.state['current']:
|
||||
self.place_buy(pair)
|
||||
|
||||
await asyncio.sleep(5)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Loop error: {e}")
|
||||
await asyncio.sleep(5)
|
||||
|
||||
if __name__ == "__main__":
|
||||
bot = TradingBotV5Enhanced()
|
||||
asyncio.run(bot.run())
|
||||
|
|
@ -0,0 +1,205 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Trading Bot V5 ENHANCED - Risk Management FIXED
|
||||
Implementiert: SL (mit korrekter Precision), TP, Daily Limit, R:R Ratio
|
||||
FIXED: PRICE_FILTER für SL Orders durch Tick-Rounding
|
||||
"""
|
||||
import os, asyncio, logging, random, json, time, math
|
||||
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 = 5 # 5% random signal
|
||||
self.INVESTMENT_PERCENT = 25 # 25% per trade
|
||||
self.STOP_LOSS_PERCENT = 2.5 # -2.5%
|
||||
self.TAKE_PROFIT_PERCENT = 3.0 # +3%
|
||||
self.DAILY_LOSS_LIMIT = -5 # -5% max
|
||||
|
||||
self.active_trades = {}
|
||||
self.daily_pnl = 0
|
||||
self.paused = False
|
||||
|
||||
# Precision cache
|
||||
self.pair_precision = {}
|
||||
self._load_pair_precision()
|
||||
|
||||
logger.info("✅ Bot initialized with Risk Management (SL 2.5%, TP 3%, Daily Limit 5%)")
|
||||
|
||||
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)
|
||||
}
|
||||
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"""
|
||||
tick = self.pair_precision.get(pair, {}).get('tick', 0.01)
|
||||
return round(price / tick) * tick
|
||||
|
||||
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
|
||||
|
||||
# Place market buy
|
||||
order = self.client.order_market_buy(symbol=pair, quantity=qty)
|
||||
logger.info(f"🟢 BUY: {pair} x{qty:.6f} @ ${entry_price:.2f}")
|
||||
|
||||
# Store trade
|
||||
self.active_trades[pair] = {
|
||||
'entry': entry_price,
|
||||
'qty': qty,
|
||||
'time': datetime.now()
|
||||
}
|
||||
|
||||
# Place SL order (FIXED WITH ROUNDING)
|
||||
await self.place_stop_loss(pair, entry_price, qty)
|
||||
|
||||
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"""
|
||||
try:
|
||||
# Calculate SL price with 2.5% 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)
|
||||
|
||||
# Place SL order
|
||||
order = self.client.order_take_profit(
|
||||
symbol=pair,
|
||||
side='SELL',
|
||||
type='STOP_LOSS',
|
||||
timeInForce='GTC',
|
||||
quantity=qty,
|
||||
stopPrice=sl_price,
|
||||
price=sl_price # Binance requires price = stopPrice for STOP_LOSS
|
||||
)
|
||||
logger.info(f"🛡️ SL: {pair} @ ${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 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
|
||||
|
||||
# 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
|
||||
|
||||
async def run_cycle(self):
|
||||
"""Main trading cycle"""
|
||||
while True:
|
||||
try:
|
||||
# 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())
|
||||
Loading…
Reference in New Issue