Mexc Scalp Bot Documentation
Mexc Scalp Bot Documentation
#!/usr/bin/env python3
"""
Bitget Scalp Trading Bot
========================
A comprehensive automated scalp trading bot for Bitget exchange using Spot REST API.
Features candlestick pattern detection, risk management, and modular architecture.
Author: AI Assistant
License: MIT
"""
import os
import time
import hmac
import hashlib
import base64
import requests
import pandas as pd
import numpy as np
import csv
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Tuple
from dotenv import load_dotenv
import json
import logging
from dataclasses import dataclass
from collections import deque
import threading
from concurrent.futures import ThreadPoolExecutor
# Load environment variables
load_dotenv()
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('trading_bot.log'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
@dataclass
class TechnicalIndicators:
"""Data class for technical indicators"""
rsi: float = 0.0
macd: float = 0.0
macd_signal: float = 0.0
bb_upper: float = 0.0
bb_lower: float = 0.0
bb_middle: float = 0.0
ema_fast: float = 0.0
ema_slow: float = 0.0
volume_sma: float = 0.0
atr: float = 0.0
@dataclass
class TradeSignal:
"""Data class for trade signals"""
symbol: str
signal_type: str # 'BUY', 'SELL', 'HOLD'
confidence: float # 0.0 to 1.0
patterns_detected: List[str]
indicators: TechnicalIndicators
timestamp: datetime
price: float
reason: str
class TechnicalAnalysis:
"""Technical analysis and indicator calculation class"""
@staticmethod
def calculate_rsi(prices: pd.Series, period: int = 14) -> pd.Series:
"""Calculate Relative Strength Index"""
delta = prices.diff()
gain = (delta.where(delta > 0, 0)).rolling(window=period).mean()
loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean()
rs = gain / loss
rsi = 100 - (100 / (1 + rs))
return rsi
@staticmethod
def calculate_macd(prices: pd.Series, fast: int = 12, slow: int = 26, signal: in
t = 9) -> Tuple[pd.Series, pd.Series, pd.Series]:
"""Calculate MACD, MACD Signal, and MACD Histogram"""
ema_fast = prices.ewm(span=fast).mean()
ema_slow = prices.ewm(span=slow).mean()
macd = ema_fast - ema_slow
macd_signal = macd.ewm(span=signal).mean()
macd_histogram = macd - macd_signal
return macd, macd_signal, macd_histogram
@staticmethod
def calculate_bollinger_bands(prices: pd.Series, period: int = 20, std_dev: int
= 2) -> Tuple[pd.Series, pd.Series, pd.Series]:
"""Calculate Bollinger Bands"""
sma = prices.rolling(window=period).mean()
std = prices.rolling(window=period).std()
upper_band = sma + (std * std_dev)
lower_band = sma - (std * std_dev)
return upper_band, sma, lower_band
@staticmethod
def calculate_ema(prices: pd.Series, period: int) -> pd.Series:
"""Calculate Exponential Moving Average"""
return prices.ewm(span=period).mean()
@staticmethod
def calculate_atr(high: pd.Series, low: pd.Series, close: pd.Series, period: int
= 14) -> pd.Series:
"""Calculate Average True Range"""
high_low = high - low
high_close = np.abs(high - close.shift())
low_close = np.abs(low - close.shift())
true_range = np.maximum(high_low, np.maximum(high_close, low_close))
atr = true_range.rolling(window=period).mean()
return atr
class PerformanceMonitor:
"""Monitor and track bot performance metrics"""
def __init__(self):
self.trades_history = deque(maxlen=1000) # Store last 1000 trades
self.daily_pnl = {}
self.win_streak = 0
self.loss_streak = 0
self.max_win_streak = 0
self.max_loss_streak = 0
self.total_trades = 0
self.winning_trades = 0
self.losing_trades = 0
self.total_pnl = 0.0
self.max_drawdown = 0.0
self.peak_balance = 0.0
self.trades_history.append(trade_data)
self.total_trades += 1
self.total_pnl += pnl
if self.peak_balance > 0:
drawdown = (self.peak_balance - current_balance) / self.peak_balance
self.max_drawdown = max(self.max_drawdown, drawdown)
return {
'total_trades': self.total_trades,
'winning_trades': self.winning_trades,
'losing_trades': self.losing_trades,
'win_rate': win_rate,
'total_pnl': self.total_pnl,
'avg_win': avg_win,
'avg_loss': avg_loss,
'profit_factor': profit_factor,
'max_win_streak': self.max_win_streak,
'max_loss_streak': self.max_loss_streak,
'max_drawdown': self.max_drawdown,
'current_streak': self.win_streak if self.win_streak > 0 else -self.loss
_streak
}
class BitgetTradingBot:
"""
Bitget Scalp Trading Bot with pattern detection and risk management
"""
def __init__(self):
"""Initialize the trading bot with API credentials and configuration"""
self.api_key = os.getenv('BITGET_API_KEY')
self.api_secret = os.getenv('BITGET_API_SECRET')
self.api_passphrase = os.getenv('BITGET_PASSPHRASE') # Bitget requires pass
phrase
self.base_url = 'https://api.bitget.com'
# Trading configuration
self.symbol = ""
self.profit_target = 0.002 # 0.2%
self.stop_loss = 0.003 # -0.3%
self.max_position_percent = 0.05 # 5% of balance
self.order_timeout = 60 # seconds
self.price_adjustment = 0.0001 # Small adjustment for better execution
self.max_spread_percent = 0.001 # 0.1% max spread
self.min_depth_threshold = 1000 # Minimum depth in USDT
# Initialize components
self.technical_analyzer = TechnicalAnalysis()
self.performance_monitor = PerformanceMonitor()
def reset_daily_counters(self):
"""Reset daily counters if new day"""
today = datetime.now().date()
if today != self.last_reset_date:
self.daily_trade_count = 0
self.last_reset_date = today
logger.info("Daily counters reset for new trading day")
def init_csv_logging(self):
"""Initialize CSV file for trade logging"""
self.csv_filename = f"trades_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
with open(self.csv_filename, 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow([
'timestamp', 'action', 'symbol', 'price', 'quantity',
'order_id', 'pnl', 'balance_usdt', 'pattern_detected'
])
def log_trade(self, action: str, symbol: str, price: float, quantity: float,
order_id: str = "", pnl: float = 0, pattern: str = ""):
"""Log trade action to CSV file"""
try:
balance = self.get_balance('USDT')
with open(self.csv_filename, 'a', newline='') as f:
writer = csv.writer(f)
writer.writerow([
datetime.now().isoformat(),
action,
symbol,
price,
quantity,
order_id,
pnl,
balance,
pattern
])
except Exception as e:
logger.error(f"Failed to log trade: {e}")
def _generate_signature(self, method: str, request_path: str, body: str = '') ->
str:
"""Generate signature for Bitget API authentication"""
timestamp = str(int(time.time() * 1000))
message = timestamp + method.upper() + request_path + body
signature = base64.b64encode(
hmac.new(
self.api_secret.encode('utf-8'),
message.encode('utf-8'),
hashlib.sha256
).digest()
).decode('utf-8')
if signed:
body = json.dumps(params) if params and method != 'GET' else ''
signature, timestamp = self._generate_signature(method, endpoint
, body)
headers.update({
'ACCESS-KEY': self.api_key,
'ACCESS-SIGN': signature,
'ACCESS-TIMESTAMP': timestamp,
'ACCESS-PASSPHRASE': self.api_passphrase,
})
if method == 'GET':
response = requests.get(url, params=params, headers=headers, tim
eout=10)
elif method == 'POST':
response = requests.post(url, json=params, headers=headers, time
out=10)
elif method == 'DELETE':
response = requests.delete(url, json=params, headers=headers, ti
meout=10)
response.raise_for_status()
result = response.json()
except requests.exceptions.RequestException as e:
logger.warning(f"Request attempt {attempt + 1} failed: {e}")
if attempt == retries - 1:
raise
time.sleep(2 ** attempt) # Exponential backoff
if isinstance(response, list):
for s in response:
if s['symbol'] == symbol:
return s
def get_candles(self, symbol: str, granularity: str = '1min', limit: int = 100)
-> pd.DataFrame:
"""
Fetch candlestick data from Bitget API
Args:
symbol: Trading pair (e.g., 'BTCUSDT')
granularity: Kline interval (1min, 5min, 15min, etc.)
limit: Number of candles to fetch
Returns:
DataFrame with OHLCV data
"""
try:
params = {
'symbol': symbol,
'granularity': granularity,
'limit': str(limit)
}
if not response:
return pd.DataFrame()
except Exception as e:
logger.error(f"Failed to fetch candles: {e}")
return pd.DataFrame()
prev_candle = df.iloc[-2]
curr_candle = df.iloc[-1]
candle = df.iloc[-1]
body_size = abs(candle['close'] - candle['open'])
upper_shadow = candle['high'] - max(candle['open'], candle['close'])
lower_shadow = min(candle['open'], candle['close']) - candle['low']
# Hammer characteristics
small_body = body_size < (candle['high'] - candle['low']) * 0.3
long_lower_shadow = lower_shadow > body_size * 2
short_upper_shadow = upper_shadow < body_size * 0.5
candle = df.iloc[-1]
body_size = abs(candle['close'] - candle['open'])
total_range = candle['high'] - candle['low']
prev_candle = df.iloc[-2]
curr_candle = df.iloc[-1]
Returns:
Dictionary with pattern names and detection results
"""
patterns = {
'bullish_engulfing': self.detect_bullish_engulfing(df),
'hammer': self.detect_hammer(df),
'doji': self.detect_doji(df),
'bearish_engulfing': self.detect_bearish_engulfing(df)
}
return patterns
try:
close_prices = df['close']
high_prices = df['high']
low_prices = df['low']
volume = df['volume']
# Calculate indicators
rsi = self.technical_analyzer.calculate_rsi(close_prices).iloc[-1]
macd, macd_signal, _ = self.technical_analyzer.calculate_macd(close_pric
es)
bb_upper, bb_middle, bb_lower = self.technical_analyzer.calculate_bollin
ger_bands(close_prices)
ema_fast = self.technical_analyzer.calculate_ema(close_prices, 12).iloc[
-1]
ema_slow = self.technical_analyzer.calculate_ema(close_prices, 26).iloc[
-1]
volume_sma = volume.rolling(window=20).mean().iloc[-1]
atr = self.technical_analyzer.calculate_atr(high_prices, low_prices, clo
se_prices).iloc[-1]
return TechnicalIndicators(
rsi=rsi if not pd.isna(rsi) else 50.0,
macd=macd.iloc[-1] if not pd.isna(macd.iloc[-1]) else 0.0,
macd_signal=macd_signal.iloc[-1] if not pd.isna(macd_signal.iloc[-1]
) else 0.0,
bb_upper=bb_upper.iloc[-1] if not pd.isna(bb_upper.iloc[-1]) else 0.
0,
bb_lower=bb_lower.iloc[-1] if not pd.isna(bb_lower.iloc[-1]) else 0.
0,
bb_middle=bb_middle.iloc[-1] if not pd.isna(bb_middle.iloc[-1]) else
0.0,
ema_fast=ema_fast if not pd.isna(ema_fast) else 0.0,
ema_slow=ema_slow if not pd.isna(ema_slow) else 0.0,
volume_sma=volume_sma if not pd.isna(volume_sma) else 0.0,
atr=atr if not pd.isna(atr) else 0.0
)
except Exception as e:
logger.error(f"Error calculating technical indicators: {e}")
return TechnicalIndicators()
# Initialize signal
signal_type = 'HOLD'
confidence = 0.0
reasons = []
patterns_detected = []
# Pattern-based signals
pattern_score = 0.0
if patterns['bullish_engulfing']:
pattern_score += 0.3
patterns_detected.append('bullish_engulfing')
reasons.append('Bullish engulfing pattern')
if patterns['hammer']:
pattern_score += 0.25
patterns_detected.append('hammer')
reasons.append('Hammer pattern')
if patterns['doji']:
pattern_score += 0.1
patterns_detected.append('doji')
reasons.append('Doji indecision')
# RSI conditions
if indicators.rsi < 30: # Oversold
indicator_score += 0.2
reasons.append(f'RSI oversold ({indicators.rsi:.1f})')
elif indicators.rsi > 70: # Overbought
indicator_score -= 0.2
reasons.append(f'RSI overbought ({indicators.rsi:.1f})')
# MACD conditions
if indicators.macd > indicators.macd_signal:
indicator_score += 0.15
reasons.append('MACD bullish crossover')
else:
indicator_score -= 0.1
# Bollinger Bands
if current_price <= indicators.bb_lower:
indicator_score += 0.15
reasons.append('Price at BB lower band')
elif current_price >= indicators.bb_upper:
indicator_score -= 0.15
reasons.append('Price at BB upper band')
# EMA trend
if indicators.ema_fast > indicators.ema_slow:
indicator_score += 0.1
reasons.append('EMA uptrend')
else:
indicator_score -= 0.05
# Volume confirmation
current_volume = df['volume'].iloc[-1]
if current_volume > indicators.volume_sma * 1.2:
indicator_score += 0.1
reasons.append('High volume confirmation')
return TradeSignal(
symbol=self.symbol,
signal_type=signal_type,
confidence=confidence,
patterns_detected=patterns_detected,
indicators=indicators,
timestamp=datetime.now(),
price=current_price,
reason='; '.join(reasons)
)
response['spread_percent'] = spread
response['bid_depth_usdt'] = bid_depth
response['ask_depth_usdt'] = ask_depth
response['best_bid'] = best_bid
response['best_ask'] = best_ask
return response
except Exception as e:
logger.error(f"Failed to get order book: {e}")
return {}
if not order_book:
return False
# Check spread
spread = order_book.get('spread_percent', float('inf'))
if spread > self.max_spread_percent:
logger.warning(f"Spread too wide: {spread:.4f}%")
return False
# Check depth
bid_depth = order_book.get('bid_depth_usdt', 0)
ask_depth = order_book.get('ask_depth_usdt', 0)
except Exception as e:
logger.error(f"Failed to check market conditions: {e}")
return False
if isinstance(response, list):
for balance in response:
if balance.get('coinName') == asset:
return float(balance.get('available', 0))
return 0.0
except Exception as e:
logger.error(f"Failed to get balance: {e}")
return 0.0
if order_type.lower() == 'limit':
params['price'] = str(price)
if response:
order_id = response.get('orderId')
logger.info(f"Order placed: {side} {quantity} {symbol} at {price} (I
D: {order_id})")
return response
except Exception as e:
logger.error(f"Failed to place order: {e}")
return {}
if response:
logger.info(f"Order cancelled: {order_id}")
self.active_orders.pop(order_id, None)
return True
return False
except Exception as e:
logger.error(f"Failed to cancel order: {e}")
return False
except Exception as e:
logger.error(f"Failed to calculate position size: {e}")
return 0.0
return adjusted_quantity
if entry_side == 'BUY':
# Long position
profit_pct = (current_price - entry_price) / entry_price
except Exception as e:
logger.error(f"Failed to check exit conditions: {e}")
return False, ""
if signal.signal_type != 'BUY':
logger.info("No strong buy signal detected, skipping trade")
return
if quantity <= 0:
logger.warning("Calculated quantity is 0 or negative")
return
buy_order_id = buy_order['orderId']
patterns_str = ','.join(signal.patterns_detected)
self.log_trade('BUY_ORDER', symbol, entry_price, quantity, buy_order_id,
0, patterns_str)
self.daily_trade_count += 1
self.last_trade_time = time.time()
order_filled = True
break
time.sleep(5)
if should_exit:
logger.info(f"Exit condition met: {reason}")
if sell_order:
sell_order_id = sell_order['orderId']
if sell_status.get('state') == 'filled':
exit_price_actual = float(sell_status['priceAvg'])
exit_qty = float(sell_status['size'])
# Remove position
del self.positions[symbol]
break
time.sleep(2)
else:
# Sell order timeout - use market order
logger.warning("Sell limit order timeout, placing market
order")
self.cancel_order(symbol, sell_order_id)
except Exception as e:
logger.error(f"Error in trade cycle: {e}")
return {
'current_balance': current_balance,
'total_trades': stats['total_trades'],
'win_rate': stats['win_rate'],
'total_pnl': stats['total_pnl'],
'max_drawdown': stats['max_drawdown'],
'profit_factor': stats['profit_factor'],
'active_positions': len(self.positions),
'daily_trades': self.daily_trade_count
}
except Exception as e:
logger.error(f"Error getting performance summary: {e}")
return {}
def emergency_stop(self):
"""Emergency stop - cancel all orders and close positions"""
logger.warning("Emergency stop initiated!")
try:
# Cancel all active orders
for order_id in list(self.active_orders.keys()):
order = self.active_orders[order_id]
self.cancel_order(order['symbol'], order_id)
except Exception as e:
logger.error(f"Error during emergency stop: {e}")
Args:
symbol: Trading pair (e.g., 'BTCUSDT')
profit_target: Profit target as decimal (0.002 = 0.2%)
stop_loss: Stop loss as decimal (0.003 = 0.3%)
max_position_percent: Max position size as % of balance
"""
self.symbol = symbol
self.profit_target = profit_target
self.stop_loss = stop_loss
self.max_position_percent = max_position_percent
try:
while True:
self.execute_trade_cycle(symbol)
if self._cycle_count % 10 == 0:
summary = self.get_performance_summary()
if summary:
logger.info(f"Performance Summary: "
f"Trades: {summary.get('total_trades', 0)}, "
f"Win Rate: {summary.get('win_rate', 0):.1%}, "
f"P&L;: ${summary.get('total_pnl', 0):.2f}, "
f"Balance: ${summary.get('current_balance', 0):.2f
}")
except KeyboardInterrupt:
logger.info("Bot stopped by user")
self.emergency_stop()
except Exception as e:
logger.error(f"Bot error: {e}")
self.emergency_stop()
finally:
# Clean up any remaining open orders
for order_id in list(self.active_orders.keys()):
order = self.active_orders[order_id]
self.cancel_order(order['symbol'], order_id)
def main():
"""Main function to run the trading bot"""
print("Bitget Scalp Trading Bot")
print("=" * 50)
# Configuration
symbol = input("Enter trading pair (e.g., BTCUSDT): ").upper()
try:
profit_target = float(input("Enter profit target % (default 0.2): ") or "0.2
") / 100
stop_loss = float(input("Enter stop loss % (default 0.3): ") or "0.3") / 100
max_position = float(input("Enter max position % of balance (default 5): ")
or "5") / 100
except ValueError:
print("Invalid input, using default values")
profit_target = 0.002
stop_loss = 0.003
max_position = 0.05