From a870b8858166a971dfec4df8d3df398aaa187bee Mon Sep 17 00:00:00 2001 From: rizary Date: Tue, 15 Jul 2025 08:17:22 +0700 Subject: [PATCH] initial trading bot --- trading_bot/Dockerfile | 73 +++ trading_bot/README.md | 348 +++++++++++ trading_bot/bot_core/bot.py | 682 ++++++++++++++++++++++ trading_bot/bot_core/config.py | 198 +++++++ trading_bot/bot_core/indicators.py | 668 +++++++++++++++++++++ trading_bot/bot_core/logger.py | 263 +++++++++ trading_bot/bot_core/order_manager.py | 544 +++++++++++++++++ trading_bot/bot_core/position_manager.py | 469 +++++++++++++++ trading_bot/bot_core/risk_manager.py | 405 +++++++++++++ trading_bot/bot_core/websocket_manager.py | 455 +++++++++++++++ trading_bot/docker-compose.yml | 224 +++++++ trading_bot/env_example | 183 ++++++ trading_bot/main.py | 145 +++++ trading_bot/requirements.txt | 70 +++ 14 files changed, 4727 insertions(+) create mode 100644 trading_bot/Dockerfile create mode 100644 trading_bot/README.md create mode 100644 trading_bot/bot_core/bot.py create mode 100644 trading_bot/bot_core/config.py create mode 100644 trading_bot/bot_core/indicators.py create mode 100644 trading_bot/bot_core/logger.py create mode 100644 trading_bot/bot_core/order_manager.py create mode 100644 trading_bot/bot_core/position_manager.py create mode 100644 trading_bot/bot_core/risk_manager.py create mode 100644 trading_bot/bot_core/websocket_manager.py create mode 100644 trading_bot/docker-compose.yml create mode 100644 trading_bot/env_example create mode 100644 trading_bot/main.py create mode 100644 trading_bot/requirements.txt diff --git a/trading_bot/Dockerfile b/trading_bot/Dockerfile new file mode 100644 index 0000000..c7715c9 --- /dev/null +++ b/trading_bot/Dockerfile @@ -0,0 +1,73 @@ +# Trading Bot Dockerfile +# Multi-stage build for optimized production image + +FROM python:3.11-slim as builder + +# Set environment variables +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 +ENV PIP_NO_CACHE_DIR=1 +ENV PIP_DISABLE_PIP_VERSION_CHECK=1 + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + build-essential \ + curl \ + git \ + && rm -rf /var/lib/apt/lists/* + +# Create and set working directory +WORKDIR /app + +# Copy requirements and install Python dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY . . + +# Production stage +FROM python:3.11-slim as production + +# Set environment variables +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 +ENV PYTHONPATH=/app + +# Install minimal runtime dependencies +RUN apt-get update && apt-get install -y \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Create non-root user for security +RUN groupadd -r tradingbot && useradd -r -g tradingbot tradingbot + +# Create application directory +WORKDIR /app + +# Copy Python packages from builder stage +COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages +COPY --from=builder /usr/local/bin /usr/local/bin + +# Copy application code +COPY --chown=tradingbot:tradingbot . . + +# Create necessary directories +RUN mkdir -p /app/logs /app/data && \ + chown -R tradingbot:tradingbot /app/logs /app/data + +# Create volume mount points +VOLUME ["/app/logs", "/app/data"] + +# Switch to non-root user +USER tradingbot + +# Expose port (if needed for monitoring) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD python -c "import requests; requests.get('http://localhost:8000/health')" || exit 1 + +# Default command +CMD ["python", "main.py"] \ No newline at end of file diff --git a/trading_bot/README.md b/trading_bot/README.md new file mode 100644 index 0000000..5a79ac4 --- /dev/null +++ b/trading_bot/README.md @@ -0,0 +1,348 @@ +# šŸ¤– Binance Trading Bot + +Advanced automated cryptocurrency trading bot for Binance exchange with real-time WebSocket connectivity, technical indicators, and comprehensive risk management. + +## šŸš€ Features + +### Core Trading Features +- **Real-time Data**: WebSocket connection to Binance for live market data +- **Technical Indicators**: Bollinger Bands, Moving Averages (SMA/EMA), RSI, MACD +- **Multiple Strategies**: Bollinger Bands + MA, Mean Reversion, MA Crossover +- **Automated Execution**: Buy/sell orders with configurable position sizing +- **Paper Trading**: Safe testing mode without real money + +### Risk Management +- **Stop-Loss & Take-Profit**: Configurable risk/reward ratios +- **Position Sizing**: Dynamic position sizing based on risk parameters +- **Daily Loss Limits**: Automatic trading halt on daily loss thresholds +- **Maximum Drawdown Protection**: Portfolio-level risk controls +- **Emergency Stop**: Manual and automatic trading halt capabilities + +### Monitoring & Analytics +- **Performance Tracking**: Comprehensive trade and portfolio analytics +- **Real-time Logging**: Structured logging with multiple log levels +- **Health Monitoring**: WebSocket connection health and reconnection logic +- **Trade History**: Persistent storage of all trading activities + +### Deployment & Operations +- **Docker Support**: Containerized deployment with docker-compose +- **Environment Configuration**: Flexible configuration via environment variables +- **Database Integration**: SQLite and PostgreSQL support +- **Optional Monitoring**: Prometheus and Grafana integration + +## šŸ“‹ Prerequisites + +- Docker and Docker Compose (recommended) +- OR Python 3.11+ with pip +- Binance account with API keys +- Basic understanding of cryptocurrency trading + +## šŸ› ļø Quick Setup (Docker - Recommended) + +### 1. Clone and Setup +```bash +git clone +cd trading_bot + +# Copy and configure environment file +cp env_example .env +nano .env # Edit with your configuration +``` + +### 2. Configure Environment +Edit `.env` file with your settings: + +```bash +# Essential Settings +BINANCE_API_KEY=your_api_key_here +BINANCE_SECRET_KEY=your_secret_key_here +BINANCE_TESTNET=true # Start with testnet! + +# Trading Configuration +TRADING_SYMBOL=BTCUSDT +POSITION_SIZE=0.001 +PAPER_TRADING=true # Keep this true for testing! +ENABLE_TRADING=false # Set to true only when ready + +# Risk Management +STOP_LOSS_PERCENT=2.0 +TAKE_PROFIT_PERCENT=3.0 +MAX_DAILY_LOSS=5.0 +``` + +### 3. Start the Bot +```bash +# Start in paper trading mode +docker-compose up -d + +# View logs +docker-compose logs -f trading-bot + +# Stop the bot +docker-compose down +``` + +## šŸ Manual Setup (Python) + +### 1. Setup Python Environment with pyenv + +#### Install pyenv (if not already installed) + +**On Linux/macOS:** +```bash +# Install pyenv +curl https://pyenv.run | bash + +# Add to your shell profile (~/.bashrc, ~/.zshrc, etc.) +export PYENV_ROOT="$HOME/.pyenv" +export PATH="$PYENV_ROOT/bin:$PATH" +eval "$(pyenv init -)" +eval "$(pyenv virtualenv-init -)" + +# Reload your shell +source ~/.bashrc # or ~/.zshrc +``` + +**On Windows:** +```powershell +# Install pyenv-win using git +git clone https://github.com/pyenv-win/pyenv-win.git %USERPROFILE%\.pyenv + +# Add to your PATH (in PowerShell) +$env:PYENV = "$env:USERPROFILE\.pyenv\pyenv-win" +$env:PATH = "$env:PYENV\bin;$env:PYENV\shims;$env:PATH" + +# Or set permanently in system environment variables +``` + +#### Install and Configure Python +```bash +# Install Python 3.11 (recommended) +pyenv install 3.11.0 + +# Create virtual environment for the trading bot +pyenv virtualenv 3.11.0 trading-bot-env + +# Activate the environment +pyenv activate trading-bot-env + +# OR set it as local environment for this directory +pyenv local trading-bot-env +``` + +### 2. Install Dependencies +```bash +# Make sure you're in the trading bot environment +pyenv activate trading-bot-env # if not already activated + +# Install required packages +pip install -r requirements.txt +``` + +### 3. Configure Environment +```bash +cp env_example .env +# Edit .env with your configuration +``` + +### 4. Run the Bot +```bash +# Ensure environment is activated +pyenv activate trading-bot-env + +# Run the trading bot +python main.py +``` + +### 5. Deactivate Environment (when done) +```bash +pyenv deactivate +``` + +## šŸ“Š Configuration Guide + +### Trading Strategies + +#### 1. Bollinger Bands + Moving Average (Default) +```bash +STRATEGY=BOLLINGER_MA +BOLLINGER_PERIOD=20 +BOLLINGER_STD=2.0 +MA_PERIOD=50 +``` +- Buys when price touches lower Bollinger Band and is above MA +- Sells when price touches upper Bollinger Band and is below MA + +#### 2. Bollinger Bands Only +```bash +STRATEGY=BOLLINGER_ONLY +``` +- Pure mean reversion strategy +- Buys at lower band, sells at upper band + +#### 3. Moving Average Crossover +```bash +STRATEGY=MA_CROSSOVER +``` +- Buys on golden cross (fast MA crosses above slow MA) +- Sells on death cross (fast MA crosses below slow MA) + +#### 4. Mean Reversion +```bash +STRATEGY=MEAN_REVERSION +``` +- Combines MA, RSI, and price distance from mean +- Buys oversold conditions, sells overbought conditions + +### Risk Management Configuration + +```bash +# Position Sizing +POSITION_SIZE=0.001 # Size per trade +MAX_POSITIONS=1 # Maximum concurrent positions + +# Risk Limits +STOP_LOSS_PERCENT=2.0 # 2% stop loss +TAKE_PROFIT_PERCENT=3.0 # 3% take profit +MAX_DAILY_LOSS=5.0 # 5% daily loss limit +MAX_PORTFOLIO_RISK=10.0 # 10% max portfolio exposure +``` + +## šŸ”§ Advanced Configuration + +### WebSocket Settings +```bash +WS_RECONNECT_DELAY=5 # Reconnection delay in seconds +WS_PING_INTERVAL=20 # Ping interval for connection health +WS_TIMEOUT=30 # WebSocket timeout +``` + +### Logging Configuration +```bash +LOG_LEVEL=INFO # DEBUG, INFO, WARNING, ERROR, CRITICAL +LOG_FILE=logs/trading_bot.log +LOG_MAX_SIZE=10485760 # 10MB log rotation +LOG_BACKUP_COUNT=5 # Keep 5 backup files +``` + +### Database Options +```bash +# SQLite (default) +DATABASE_URL=sqlite:///data/trading_bot.db + +# PostgreSQL (advanced) +DATABASE_URL=postgresql://user:password@localhost:5432/trading_bot +``` + +## šŸ“ˆ Monitoring + +### Health Checks +- WebSocket connection status +- Order execution monitoring +- Risk limit monitoring +- Performance tracking + +### Log Analysis +```bash +# View real-time logs +docker-compose logs -f trading-bot + +# Search for specific events +docker-compose logs trading-bot | grep "SIGNAL" +docker-compose logs trading-bot | grep "TRADE" +docker-compose logs trading-bot | grep "ERROR" +``` + +### Performance Metrics +The bot tracks: +- Total trades and win rate +- PnL and portfolio value +- Risk metrics and drawdown +- Trade execution statistics + +## 🚨 Safety Guidelines + +### Before Live Trading +1. **Test Extensively**: Use paper trading for weeks/months +2. **Verify Strategy**: Backtest your strategy on historical data +3. **Start Small**: Begin with minimal position sizes +4. **Monitor Closely**: Watch the bot's behavior carefully +5. **Have Exit Plan**: Know how to stop the bot quickly + +### API Key Security +1. **Read-Only Testing**: Start with read-only API keys +2. **IP Restrictions**: Restrict API keys to your IP address +3. **No Withdrawals**: Never enable withdrawal permissions +4. **Regular Rotation**: Rotate API keys regularly + +### Risk Management +1. **Daily Limits**: Set and respect daily loss limits +2. **Position Sizing**: Never risk more than you can afford to lose +3. **Stop Losses**: Always use stop-loss orders +4. **Diversification**: Don't put all funds in one strategy + +## šŸ› Troubleshooting + +### Common Issues + +#### WebSocket Connection Problems +```bash +# Check network connectivity +docker-compose logs trading-bot | grep "WebSocket" + +# Restart WebSocket connection +docker-compose restart trading-bot +``` + +#### API Permission Errors +```bash +# Verify API key permissions +# Ensure API key has spot trading enabled (if not paper trading) +# Check IP restrictions +``` + +#### High Memory Usage +```bash +# Check container resources +docker stats trading-bot + +# Adjust log levels +LOG_LEVEL=WARNING # In .env file +``` + +### Log Levels +- **DEBUG**: Detailed technical information +- **INFO**: General operational information +- **WARNING**: Important warnings and issues +- **ERROR**: Error conditions that need attention +- **CRITICAL**: Critical errors requiring immediate action + +## šŸ“ Contributing + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Add tests if applicable +5. Submit a pull request + +## šŸ”— Related Projects + +- [CCXT](https://github.com/ccxt/ccxt) - Cryptocurrency trading library +- [pandas-ta](https://github.com/twopirllc/pandas-ta) - Technical analysis indicators +- [Binance API](https://binance-docs.github.io/apidocs/) - Official Binance API documentation + +## šŸ“„ License + +This project is for educational purposes only. Use at your own risk. + +## šŸ†˜ Support + +For questions and support: +1. Check the troubleshooting section +2. Review the logs for error messages +3. Ensure your configuration is correct +4. Test with paper trading first + +## āš ļø Final Warning + +**Cryptocurrency trading is extremely risky. This bot is provided as-is for educational purposes. The developers are not responsible for any financial losses. Always trade responsibly and never invest more than you can afford to lose.** \ No newline at end of file diff --git a/trading_bot/bot_core/bot.py b/trading_bot/bot_core/bot.py new file mode 100644 index 0000000..8ae0d5f --- /dev/null +++ b/trading_bot/bot_core/bot.py @@ -0,0 +1,682 @@ +""" +Main Trading Bot +Orchestrates all components including WebSocket, indicators, orders, and risk management +""" + +import asyncio +import json +import time +from datetime import datetime, timedelta +from typing import Dict, List, Optional, Any +from dataclasses import dataclass, field + +import ccxt +import pandas as pd +import numpy as np + +from .config import Config +from .logger import TradingLogger +from .websocket_manager import WebSocketManager +from .indicators import IndicatorManager +from .order_manager import OrderManager +from .risk_manager import RiskManager +from .position_manager import PositionManager +from .performance_tracker import PerformanceTracker + + +@dataclass +class TradingSignal: + """Trading signal data structure""" + symbol: str + signal_type: str # 'BUY', 'SELL', 'HOLD' + strength: float # Signal strength 0-1 + price: float + timestamp: datetime + indicators: Dict[str, Any] = field(default_factory=dict) + reason: str = "" + + +class TradingBot: + """ + Main trading bot class that orchestrates all components + """ + + def __init__(self, config: Config, logger: TradingLogger): + self.config = config + self.logger = logger + + # Core components + self.exchange: ccxt.Exchange = None + self.websocket_manager: WebSocketManager = None + self.indicator_manager: IndicatorManager = None + self.order_manager: OrderManager = None + self.risk_manager: RiskManager = None + self.position_manager: PositionManager = None + self.performance_tracker: PerformanceTracker = None + + # Bot state + self.running = False + self.initialized = False + self.last_signal: Optional[TradingSignal] = None + self.market_data: Dict[str, Any] = {} + self.current_price: float = 0.0 + + # Performance tracking + self.trade_count = 0 + self.successful_trades = 0 + self.failed_trades = 0 + self.total_pnl = 0.0 + self.session_start = datetime.now() + + # Data storage + self.price_history: List[Dict] = [] + self.signal_history: List[TradingSignal] = [] + + # Async tasks + self.tasks: List[asyncio.Task] = [] + + async def initialize(self): + """Initialize all bot components""" + if self.initialized: + return + + self.logger.info("šŸ”§ Initializing trading bot components...") + + try: + # Initialize exchange + await self._initialize_exchange() + + # Initialize components + await self._initialize_components() + + # Validate configuration + await self._validate_setup() + + self.initialized = True + self.logger.info("āœ… Bot initialization completed successfully") + + except Exception as e: + self.logger.error(f"āŒ Bot initialization failed: {e}") + raise + + async def _initialize_exchange(self): + """Initialize CCXT exchange connection""" + try: + self.logger.info("šŸ”Œ Initializing Binance exchange connection...") + + # Create exchange instance + exchange_config = self.config.get_binance_config() + self.exchange = ccxt.binance(exchange_config) + + # Test connection + if not self.config.is_paper_trading(): + await self.exchange.load_markets() + balance = await self.exchange.fetch_balance() + self.logger.info(f"āœ… Exchange connected. Available balance: {balance.get('USDT', {}).get('free', 0)} USDT") + else: + self.logger.info("šŸ“„ Paper trading mode - Exchange connection simulated") + + except Exception as e: + self.logger.error(f"āŒ Exchange initialization failed: {e}") + raise + + async def _initialize_components(self): + """Initialize all bot components""" + + # WebSocket manager for real-time data + self.websocket_manager = WebSocketManager( + self.config, + self.logger, + self._on_websocket_message + ) + + # Indicator manager for technical analysis + self.indicator_manager = IndicatorManager( + self.config, + self.logger + ) + + # Order manager for trade execution + self.order_manager = OrderManager( + self.config, + self.logger, + self.exchange + ) + + # Risk manager for position sizing and risk control + self.risk_manager = RiskManager( + self.config, + self.logger + ) + + # Position manager for tracking positions + self.position_manager = PositionManager( + self.config, + self.logger, + self.exchange + ) + + # Performance tracker for metrics + self.performance_tracker = PerformanceTracker( + self.config, + self.logger + ) + + self.logger.info("āœ… All components initialized") + + async def _validate_setup(self): + """Validate bot setup and configuration""" + + # Check symbol exists + if not self.config.is_paper_trading(): + markets = await self.exchange.load_markets() + if self.config.SYMBOL not in markets: + raise ValueError(f"Symbol {self.config.SYMBOL} not found in markets") + + # Validate minimum balance for trading + if not self.config.is_paper_trading() and self.config.ENABLE_TRADING: + balance = await self.exchange.fetch_balance() + usdt_balance = balance.get('USDT', {}).get('free', 0) + + min_balance = self.config.POSITION_SIZE * self.current_price * 1.1 # 10% buffer + if usdt_balance < min_balance: + self.logger.warning(f"āš ļø Low balance: {usdt_balance} USDT (minimum recommended: {min_balance})") + + self.logger.info("āœ… Setup validation completed") + + async def start(self): + """Start the trading bot""" + if not self.initialized: + await self.initialize() + + if self.running: + self.logger.warning("āš ļø Bot is already running") + return + + self.running = True + self.logger.info("šŸš€ Starting trading bot...") + + try: + # Start WebSocket connection + await self.websocket_manager.start() + + # Start background tasks + self.tasks = [ + asyncio.create_task(self._trading_loop()), + asyncio.create_task(self._monitoring_loop()), + asyncio.create_task(self._risk_monitoring_loop()), + asyncio.create_task(self._performance_update_loop()) + ] + + # Wait for all tasks + await asyncio.gather(*self.tasks) + + except Exception as e: + self.logger.error(f"āŒ Bot encountered error: {e}") + await self.stop() + raise + + async def stop(self): + """Stop the trading bot gracefully""" + if not self.running: + return + + self.logger.info("šŸ›‘ Stopping trading bot...") + self.running = False + + # Cancel all tasks + for task in self.tasks: + task.cancel() + + # Stop WebSocket + if self.websocket_manager: + await self.websocket_manager.stop() + + # Cancel all open orders + if self.order_manager: + await self.order_manager.cancel_all_orders() + + # Close all positions if configured + if self.position_manager and self.config.ENABLE_TRADING: + await self.position_manager.close_all_positions() + + # Save performance data + if self.performance_tracker: + await self.performance_tracker.save_session_data() + + self.logger.info("āœ… Bot stopped successfully") + + async def _trading_loop(self): + """Main trading loop""" + self.logger.info("šŸŽÆ Starting trading loop...") + + while self.running: + try: + # Check if we have sufficient data + if not self.market_data or not self.current_price: + await asyncio.sleep(1) + continue + + # Generate trading signal + signal = await self._generate_trading_signal() + + if signal: + self.last_signal = signal + self.signal_history.append(signal) + + # Execute signal if conditions are met + await self._execute_signal(signal) + + # Update positions + await self.position_manager.update_positions() + + # Check for position exits + await self._check_position_exits() + + # Brief pause before next iteration + await asyncio.sleep(1) + + except Exception as e: + self.logger.error(f"āŒ Error in trading loop: {e}") + await asyncio.sleep(5) + + async def _monitoring_loop(self): + """Monitor bot health and performance""" + self.logger.info("šŸ“Š Starting monitoring loop...") + + while self.running: + try: + # Log current status + positions = await self.position_manager.get_positions() + open_orders = await self.order_manager.get_open_orders() + + status = { + 'current_price': self.current_price, + 'positions': len(positions), + 'open_orders': len(open_orders), + 'last_signal': self.last_signal.signal_type if self.last_signal else 'NONE' + } + + self.logger.debug(f"šŸ“ˆ Status: {status}") + + # Check for stale data + if self.market_data: + last_update = self.market_data.get('timestamp', 0) + if time.time() - last_update > 60: # 1 minute + self.logger.warning("āš ļø Market data is stale, checking connection...") + await self.websocket_manager.reconnect() + + await asyncio.sleep(30) # Monitor every 30 seconds + + except Exception as e: + self.logger.error(f"āŒ Error in monitoring loop: {e}") + await asyncio.sleep(10) + + async def _risk_monitoring_loop(self): + """Monitor risk limits and safety checks""" + self.logger.info("šŸ›”ļø Starting risk monitoring loop...") + + while self.running: + try: + # Check daily loss limit + daily_pnl = await self.performance_tracker.get_daily_pnl() + if daily_pnl < -self.config.MAX_DAILY_LOSS: + self.logger.warning(f"āš ļø Daily loss limit exceeded: {daily_pnl:.2f}%") + await self.position_manager.close_all_positions() + await self.order_manager.cancel_all_orders() + + # Check maximum positions + positions = await self.position_manager.get_positions() + if len(positions) > self.config.MAX_POSITIONS: + self.logger.warning(f"āš ļø Maximum positions exceeded: {len(positions)}") + + # Check for stuck orders + await self.order_manager.check_stuck_orders() + + await asyncio.sleep(60) # Check every minute + + except Exception as e: + self.logger.error(f"āŒ Error in risk monitoring: {e}") + await asyncio.sleep(30) + + async def _performance_update_loop(self): + """Update performance metrics""" + self.logger.info("šŸ“ˆ Starting performance update loop...") + + while self.running: + try: + # Update performance metrics + await self.performance_tracker.update_metrics() + + # Log performance summary + if self.trade_count > 0: + win_rate = (self.successful_trades / self.trade_count) * 100 + self.logger.performance({ + 'total_trades': self.trade_count, + 'win_rate': f"{win_rate:.1f}%", + 'total_pnl': f"{self.total_pnl:.2f}", + 'uptime': str(datetime.now() - self.session_start) + }) + + await asyncio.sleep(self.config.PERFORMANCE_UPDATE_INTERVAL) + + except Exception as e: + self.logger.error(f"āŒ Error in performance update: {e}") + await asyncio.sleep(30) + + async def _generate_trading_signal(self) -> Optional[TradingSignal]: + """Generate trading signal based on indicators""" + + try: + # Get latest price data + if len(self.price_history) < self.config.LOOKBACK_PERIODS: + return None + + # Create DataFrame from price history + df = pd.DataFrame(self.price_history[-self.config.LOOKBACK_PERIODS:]) + + # Calculate indicators + indicators = await self.indicator_manager.calculate_indicators(df) + + # Generate signal based on strategy + signal = await self._evaluate_strategy(indicators) + + if signal: + self.logger.signal(signal.signal_type, signal.symbol, { + 'price': signal.price, + 'strength': signal.strength, + 'indicators': signal.indicators + }) + + return signal + + except Exception as e: + self.logger.error(f"āŒ Error generating signal: {e}") + return None + + async def _evaluate_strategy(self, indicators: Dict[str, Any]) -> Optional[TradingSignal]: + """Evaluate trading strategy based on indicators""" + + try: + if self.config.STRATEGY == 'BOLLINGER_MA': + return await self._bollinger_ma_strategy(indicators) + elif self.config.STRATEGY == 'BOLLINGER_ONLY': + return await self._bollinger_only_strategy(indicators) + elif self.config.STRATEGY == 'MA_CROSSOVER': + return await self._ma_crossover_strategy(indicators) + elif self.config.STRATEGY == 'MEAN_REVERSION': + return await self._mean_reversion_strategy(indicators) + else: + self.logger.warning(f"āš ļø Unknown strategy: {self.config.STRATEGY}") + return None + + except Exception as e: + self.logger.error(f"āŒ Error evaluating strategy: {e}") + return None + + async def _bollinger_ma_strategy(self, indicators: Dict[str, Any]) -> Optional[TradingSignal]: + """Bollinger Bands + Moving Average strategy""" + + bb_upper = indicators.get('bb_upper') + bb_lower = indicators.get('bb_lower') + bb_middle = indicators.get('bb_middle') + ma_value = indicators.get('ma_value') + + if not all([bb_upper, bb_lower, bb_middle, ma_value]): + return None + + current_price = self.current_price + + # Buy signal: Price near lower band and above MA + if current_price <= bb_lower * 1.01 and current_price > ma_value: + return TradingSignal( + symbol=self.config.SYMBOL, + signal_type='BUY', + strength=0.8, + price=current_price, + timestamp=datetime.now(), + indicators=indicators, + reason="Price at lower Bollinger Band and above MA" + ) + + # Sell signal: Price near upper band and below MA + elif current_price >= bb_upper * 0.99 and current_price < ma_value: + return TradingSignal( + symbol=self.config.SYMBOL, + signal_type='SELL', + strength=0.8, + price=current_price, + timestamp=datetime.now(), + indicators=indicators, + reason="Price at upper Bollinger Band and below MA" + ) + + return None + + async def _bollinger_only_strategy(self, indicators: Dict[str, Any]) -> Optional[TradingSignal]: + """Bollinger Bands only strategy""" + + bb_upper = indicators.get('bb_upper') + bb_lower = indicators.get('bb_lower') + bb_squeeze = indicators.get('bb_squeeze', False) + + if not all([bb_upper, bb_lower]): + return None + + current_price = self.current_price + + # Buy signal: Price touches lower band + if current_price <= bb_lower: + strength = 0.9 if bb_squeeze else 0.7 + return TradingSignal( + symbol=self.config.SYMBOL, + signal_type='BUY', + strength=strength, + price=current_price, + timestamp=datetime.now(), + indicators=indicators, + reason="Price at lower Bollinger Band" + ) + + # Sell signal: Price touches upper band + elif current_price >= bb_upper: + strength = 0.9 if bb_squeeze else 0.7 + return TradingSignal( + symbol=self.config.SYMBOL, + signal_type='SELL', + strength=strength, + price=current_price, + timestamp=datetime.now(), + indicators=indicators, + reason="Price at upper Bollinger Band" + ) + + return None + + async def _ma_crossover_strategy(self, indicators: Dict[str, Any]) -> Optional[TradingSignal]: + """Moving Average crossover strategy""" + + ma_fast = indicators.get('ma_fast') + ma_slow = indicators.get('ma_slow') + ma_fast_prev = indicators.get('ma_fast_prev') + ma_slow_prev = indicators.get('ma_slow_prev') + + if not all([ma_fast, ma_slow, ma_fast_prev, ma_slow_prev]): + return None + + # Golden cross: fast MA crosses above slow MA + if ma_fast > ma_slow and ma_fast_prev <= ma_slow_prev: + return TradingSignal( + symbol=self.config.SYMBOL, + signal_type='BUY', + strength=0.8, + price=self.current_price, + timestamp=datetime.now(), + indicators=indicators, + reason="Golden cross - fast MA above slow MA" + ) + + # Death cross: fast MA crosses below slow MA + elif ma_fast < ma_slow and ma_fast_prev >= ma_slow_prev: + return TradingSignal( + symbol=self.config.SYMBOL, + signal_type='SELL', + strength=0.8, + price=self.current_price, + timestamp=datetime.now(), + indicators=indicators, + reason="Death cross - fast MA below slow MA" + ) + + return None + + async def _mean_reversion_strategy(self, indicators: Dict[str, Any]) -> Optional[TradingSignal]: + """Mean reversion strategy""" + + bb_middle = indicators.get('bb_middle') + rsi = indicators.get('rsi') + + if not all([bb_middle, rsi]): + return None + + current_price = self.current_price + distance_from_mean = abs(current_price - bb_middle) / bb_middle + + # Buy signal: Price below mean and RSI oversold + if current_price < bb_middle and rsi < 30 and distance_from_mean > 0.02: + return TradingSignal( + symbol=self.config.SYMBOL, + signal_type='BUY', + strength=0.7, + price=current_price, + timestamp=datetime.now(), + indicators=indicators, + reason="Mean reversion - price below mean and RSI oversold" + ) + + # Sell signal: Price above mean and RSI overbought + elif current_price > bb_middle and rsi > 70 and distance_from_mean > 0.02: + return TradingSignal( + symbol=self.config.SYMBOL, + signal_type='SELL', + strength=0.7, + price=current_price, + timestamp=datetime.now(), + indicators=indicators, + reason="Mean reversion - price above mean and RSI overbought" + ) + + return None + + async def _execute_signal(self, signal: TradingSignal): + """Execute trading signal""" + + try: + # Check if trading is enabled + if not self.config.ENABLE_TRADING: + self.logger.info(f"šŸ“„ Paper trading: {signal.signal_type} signal for {signal.symbol}") + return + + # Risk checks + if not await self.risk_manager.check_signal_risk(signal): + self.logger.warning(f"āš ļø Signal failed risk check: {signal.signal_type} {signal.symbol}") + return + + # Check for existing positions + current_positions = await self.position_manager.get_positions() + + if signal.signal_type == 'BUY': + # Don't buy if already long + if any(pos.side == 'long' for pos in current_positions): + self.logger.debug("šŸ“ˆ Already long - skipping BUY signal") + return + + # Calculate position size + position_size = await self.risk_manager.calculate_position_size(signal) + + # Place buy order + await self.order_manager.place_buy_order( + symbol=signal.symbol, + quantity=position_size, + price=signal.price + ) + + elif signal.signal_type == 'SELL': + # Don't sell if already short + if any(pos.side == 'short' for pos in current_positions): + self.logger.debug("šŸ“‰ Already short - skipping SELL signal") + return + + # Calculate position size + position_size = await self.risk_manager.calculate_position_size(signal) + + # Place sell order + await self.order_manager.place_sell_order( + symbol=signal.symbol, + quantity=position_size, + price=signal.price + ) + + except Exception as e: + self.logger.error(f"āŒ Error executing signal: {e}") + + async def _check_position_exits(self): + """Check if positions should be closed""" + + positions = await self.position_manager.get_positions() + + for position in positions: + # Check stop loss and take profit + should_close, reason = await self.risk_manager.check_position_exit(position, self.current_price) + + if should_close: + await self.position_manager.close_position(position, reason) + + # Update trade statistics + if reason.startswith('TAKE_PROFIT'): + self.successful_trades += 1 + elif reason.startswith('STOP_LOSS'): + self.failed_trades += 1 + + async def _on_websocket_message(self, message: Dict[str, Any]): + """Handle WebSocket messages""" + + try: + # Update market data + self.market_data = message + + # Extract price information + if 'price' in message: + self.current_price = float(message['price']) + + # Store price history + price_data = { + 'timestamp': message.get('timestamp', time.time()), + 'price': self.current_price, + 'volume': message.get('volume', 0) + } + self.price_history.append(price_data) + + # Keep only recent data + if len(self.price_history) > self.config.LOOKBACK_PERIODS * 2: + self.price_history = self.price_history[-self.config.LOOKBACK_PERIODS:] + + # Update order manager with market data + await self.order_manager.update_market_data(message) + + except Exception as e: + self.logger.error(f"āŒ Error processing WebSocket message: {e}") + + def get_status(self) -> Dict[str, Any]: + """Get current bot status""" + return { + 'running': self.running, + 'initialized': self.initialized, + 'current_price': self.current_price, + 'trade_count': self.trade_count, + 'successful_trades': self.successful_trades, + 'failed_trades': self.failed_trades, + 'total_pnl': self.total_pnl, + 'uptime': str(datetime.now() - self.session_start), + 'last_signal': self.last_signal.signal_type if self.last_signal else None + } \ No newline at end of file diff --git a/trading_bot/bot_core/config.py b/trading_bot/bot_core/config.py new file mode 100644 index 0000000..6364c1c --- /dev/null +++ b/trading_bot/bot_core/config.py @@ -0,0 +1,198 @@ +""" +Configuration Management for Trading Bot +Handles environment variables with validation and defaults +""" + +import os +import logging +from typing import Optional, Union +from dataclasses import dataclass, field +from dotenv import load_dotenv + + +@dataclass +class Config: + """Trading bot configuration with environment variable support""" + + # Load environment variables + def __post_init__(self): + load_dotenv() + self._load_config() + self._validate_config() + + def _load_config(self): + """Load configuration from environment variables""" + + # Binance API Configuration + self.BINANCE_API_KEY: str = os.getenv('BINANCE_API_KEY', '') + self.BINANCE_SECRET_KEY: str = os.getenv('BINANCE_SECRET_KEY', '') + self.BINANCE_TESTNET: bool = os.getenv('BINANCE_TESTNET', 'false').lower() == 'true' + + # Trading Configuration + self.SYMBOL: str = os.getenv('TRADING_SYMBOL', 'BTCUSDT') + self.POSITION_SIZE: float = float(os.getenv('POSITION_SIZE', '0.001')) + self.LEVERAGE: int = int(os.getenv('LEVERAGE', '1')) + self.MAX_POSITIONS: int = int(os.getenv('MAX_POSITIONS', '1')) + + # Risk Management + self.STOP_LOSS_PERCENT: float = float(os.getenv('STOP_LOSS_PERCENT', '2.0')) + self.TAKE_PROFIT_PERCENT: float = float(os.getenv('TAKE_PROFIT_PERCENT', '3.0')) + self.MAX_DAILY_LOSS: float = float(os.getenv('MAX_DAILY_LOSS', '5.0')) + self.MAX_PORTFOLIO_RISK: float = float(os.getenv('MAX_PORTFOLIO_RISK', '10.0')) + + # Technical Indicators + self.BOLLINGER_PERIOD: int = int(os.getenv('BOLLINGER_PERIOD', '20')) + self.BOLLINGER_STD: float = float(os.getenv('BOLLINGER_STD', '2.0')) + self.MA_PERIOD: int = int(os.getenv('MA_PERIOD', '50')) + self.MA_TYPE: str = os.getenv('MA_TYPE', 'SMA') # SMA, EMA + + # Trading Strategy + self.STRATEGY: str = os.getenv('STRATEGY', 'BOLLINGER_MA') + self.TIMEFRAME: str = os.getenv('TIMEFRAME', '1m') + self.LOOKBACK_PERIODS: int = int(os.getenv('LOOKBACK_PERIODS', '100')) + + # WebSocket Configuration + self.WS_RECONNECT_DELAY: int = int(os.getenv('WS_RECONNECT_DELAY', '5')) + self.WS_PING_INTERVAL: int = int(os.getenv('WS_PING_INTERVAL', '20')) + self.WS_TIMEOUT: int = int(os.getenv('WS_TIMEOUT', '30')) + + # Logging Configuration + self.LOG_LEVEL: str = os.getenv('LOG_LEVEL', 'INFO') + self.LOG_FILE: str = os.getenv('LOG_FILE', 'logs/trading_bot.log') + self.LOG_MAX_SIZE: int = int(os.getenv('LOG_MAX_SIZE', '10485760')) # 10MB + self.LOG_BACKUP_COUNT: int = int(os.getenv('LOG_BACKUP_COUNT', '5')) + + # Database Configuration (for tracking) + self.DATABASE_URL: str = os.getenv('DATABASE_URL', 'sqlite:///trading_bot.db') + self.TRACK_PERFORMANCE: bool = os.getenv('TRACK_PERFORMANCE', 'true').lower() == 'true' + + # Notification Configuration + self.TELEGRAM_BOT_TOKEN: str = os.getenv('TELEGRAM_BOT_TOKEN', '') + self.TELEGRAM_CHAT_ID: str = os.getenv('TELEGRAM_CHAT_ID', '') + self.ENABLE_NOTIFICATIONS: bool = os.getenv('ENABLE_NOTIFICATIONS', 'false').lower() == 'true' + + # Safety Features + self.PAPER_TRADING: bool = os.getenv('PAPER_TRADING', 'true').lower() == 'true' + self.ENABLE_TRADING: bool = os.getenv('ENABLE_TRADING', 'false').lower() == 'true' + self.TRADING_HOURS_START: str = os.getenv('TRADING_HOURS_START', '00:00') + self.TRADING_HOURS_END: str = os.getenv('TRADING_HOURS_END', '23:59') + + # Performance Monitoring + self.PERFORMANCE_UPDATE_INTERVAL: int = int(os.getenv('PERFORMANCE_UPDATE_INTERVAL', '60')) + self.SAVE_TRADES_TO_FILE: bool = os.getenv('SAVE_TRADES_TO_FILE', 'true').lower() == 'true' + self.TRADES_FILE_PATH: str = os.getenv('TRADES_FILE_PATH', 'data/trades.json') + + # Advanced Features + self.ENABLE_VOLUME_ANALYSIS: bool = os.getenv('ENABLE_VOLUME_ANALYSIS', 'false').lower() == 'true' + self.VOLUME_THRESHOLD: float = float(os.getenv('VOLUME_THRESHOLD', '1.5')) + self.ENABLE_SENTIMENT_ANALYSIS: bool = os.getenv('ENABLE_SENTIMENT_ANALYSIS', 'false').lower() == 'true' + + # Exchange Rate Limits + self.RATE_LIMIT_REQUESTS: int = int(os.getenv('RATE_LIMIT_REQUESTS', '1200')) + self.RATE_LIMIT_INTERVAL: int = int(os.getenv('RATE_LIMIT_INTERVAL', '60')) + + # Backtesting Configuration + self.BACKTEST_START_DATE: str = os.getenv('BACKTEST_START_DATE', '2024-01-01') + self.BACKTEST_END_DATE: str = os.getenv('BACKTEST_END_DATE', '2024-12-31') + self.BACKTEST_INITIAL_CAPITAL: float = float(os.getenv('BACKTEST_INITIAL_CAPITAL', '10000')) + + def _validate_config(self): + """Validate critical configuration parameters""" + + # Validate API keys if not in paper trading mode + if not self.PAPER_TRADING and self.ENABLE_TRADING: + if not self.BINANCE_API_KEY: + raise ValueError("BINANCE_API_KEY is required for live trading") + if not self.BINANCE_SECRET_KEY: + raise ValueError("BINANCE_SECRET_KEY is required for live trading") + + # Validate risk parameters + if self.STOP_LOSS_PERCENT <= 0: + raise ValueError("STOP_LOSS_PERCENT must be positive") + if self.TAKE_PROFIT_PERCENT <= 0: + raise ValueError("TAKE_PROFIT_PERCENT must be positive") + if self.POSITION_SIZE <= 0: + raise ValueError("POSITION_SIZE must be positive") + + # Validate indicator parameters + if self.BOLLINGER_PERIOD <= 0: + raise ValueError("BOLLINGER_PERIOD must be positive") + if self.BOLLINGER_STD <= 0: + raise ValueError("BOLLINGER_STD must be positive") + if self.MA_PERIOD <= 0: + raise ValueError("MA_PERIOD must be positive") + + # Validate symbol format + if not self.SYMBOL or len(self.SYMBOL) < 3: + raise ValueError("Invalid SYMBOL format") + + # Validate timeframe + valid_timeframes = ['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', '6h', '8h', '12h', '1d', '3d', '1w', '1M'] + if self.TIMEFRAME not in valid_timeframes: + raise ValueError(f"Invalid TIMEFRAME. Must be one of: {valid_timeframes}") + + # Validate MA type + valid_ma_types = ['SMA', 'EMA'] + if self.MA_TYPE not in valid_ma_types: + raise ValueError(f"Invalid MA_TYPE. Must be one of: {valid_ma_types}") + + # Validate strategy + valid_strategies = ['BOLLINGER_MA', 'BOLLINGER_ONLY', 'MA_CROSSOVER', 'MEAN_REVERSION'] + if self.STRATEGY not in valid_strategies: + raise ValueError(f"Invalid STRATEGY. Must be one of: {valid_strategies}") + + def get_binance_config(self) -> dict: + """Get Binance exchange configuration""" + config = { + 'apiKey': self.BINANCE_API_KEY, + 'secret': self.BINANCE_SECRET_KEY, + 'enableRateLimit': True, + 'options': { + 'defaultType': 'spot', # or 'future' for futures + } + } + + if self.BINANCE_TESTNET: + config['sandbox'] = True + config['urls'] = { + 'api': 'https://testnet.binance.vision/api', + 'stream': 'wss://testnet.binance.vision/ws', + } + + return config + + def get_websocket_config(self) -> dict: + """Get WebSocket configuration""" + return { + 'reconnect_delay': self.WS_RECONNECT_DELAY, + 'ping_interval': self.WS_PING_INTERVAL, + 'timeout': self.WS_TIMEOUT, + } + + def get_risk_config(self) -> dict: + """Get risk management configuration""" + return { + 'stop_loss_percent': self.STOP_LOSS_PERCENT, + 'take_profit_percent': self.TAKE_PROFIT_PERCENT, + 'max_daily_loss': self.MAX_DAILY_LOSS, + 'max_portfolio_risk': self.MAX_PORTFOLIO_RISK, + 'max_positions': self.MAX_POSITIONS, + } + + def get_indicator_config(self) -> dict: + """Get technical indicator configuration""" + return { + 'bollinger_period': self.BOLLINGER_PERIOD, + 'bollinger_std': self.BOLLINGER_STD, + 'ma_period': self.MA_PERIOD, + 'ma_type': self.MA_TYPE, + 'lookback_periods': self.LOOKBACK_PERIODS, + } + + def is_paper_trading(self) -> bool: + """Check if bot is in paper trading mode""" + return self.PAPER_TRADING or not self.ENABLE_TRADING + + def __str__(self) -> str: + """String representation of configuration""" + return f"Config(symbol={self.SYMBOL}, strategy={self.STRATEGY}, paper_trading={self.PAPER_TRADING})" \ No newline at end of file diff --git a/trading_bot/bot_core/indicators.py b/trading_bot/bot_core/indicators.py new file mode 100644 index 0000000..f08e80c --- /dev/null +++ b/trading_bot/bot_core/indicators.py @@ -0,0 +1,668 @@ +""" +Technical Indicators Manager +Calculates Bollinger Bands, Moving Averages, and other technical indicators +""" + +import numpy as np +import pandas as pd +from typing import Dict, List, Optional, Any, Tuple +from dataclasses import dataclass +from datetime import datetime + +from .config import Config +from .logger import TradingLogger + + +@dataclass +class IndicatorResult: + """Result container for indicator calculations""" + name: str + value: float + timestamp: datetime + parameters: Dict[str, Any] + metadata: Dict[str, Any] = None + + +class IndicatorManager: + """ + Manager for technical indicator calculations + """ + + def __init__(self, config: Config, logger: TradingLogger): + self.config = config + self.logger = logger + + # Indicator cache + self.indicator_cache: Dict[str, Any] = {} + self.last_calculation = None + + # Parameters + self.bollinger_period = config.BOLLINGER_PERIOD + self.bollinger_std = config.BOLLINGER_STD + self.ma_period = config.MA_PERIOD + self.ma_type = config.MA_TYPE + + self.logger.info(f"šŸ“Š Indicator manager initialized with BB({self.bollinger_period}, {self.bollinger_std}) and {self.ma_type}({self.ma_period})") + + async def calculate_indicators(self, df: pd.DataFrame) -> Dict[str, Any]: + """ + Calculate all configured indicators + + Args: + df: DataFrame with OHLCV data + + Returns: + Dictionary of indicator values + """ + + if df.empty or len(df) < max(self.bollinger_period, self.ma_period): + self.logger.warning("āš ļø Insufficient data for indicator calculation") + return {} + + try: + indicators = {} + + # Calculate Bollinger Bands + bb_result = self._calculate_bollinger_bands(df) + indicators.update(bb_result) + + # Calculate Moving Averages + ma_result = self._calculate_moving_averages(df) + indicators.update(ma_result) + + # Calculate RSI + rsi_result = self._calculate_rsi(df) + indicators.update(rsi_result) + + # Calculate additional indicators + additional_result = self._calculate_additional_indicators(df) + indicators.update(additional_result) + + # Cache results + self.indicator_cache = indicators + self.last_calculation = datetime.now() + + # Log indicator values + self.logger.indicator("ALL_INDICATORS", self.config.SYMBOL, indicators) + + return indicators + + except Exception as e: + self.logger.error(f"āŒ Error calculating indicators: {e}") + return {} + + def _calculate_bollinger_bands(self, df: pd.DataFrame) -> Dict[str, Any]: + """ + Calculate Bollinger Bands + + Args: + df: DataFrame with price data + + Returns: + Dictionary with BB values + """ + + try: + # Use close prices for calculation + close_prices = df['close'] if 'close' in df.columns else df['price'] + + # Calculate moving average (middle band) + sma = close_prices.rolling(window=self.bollinger_period).mean() + + # Calculate standard deviation + std = close_prices.rolling(window=self.bollinger_period).std() + + # Calculate upper and lower bands + upper_band = sma + (std * self.bollinger_std) + lower_band = sma - (std * self.bollinger_std) + + # Get current values + current_price = close_prices.iloc[-1] + current_upper = upper_band.iloc[-1] + current_lower = lower_band.iloc[-1] + current_middle = sma.iloc[-1] + + # Calculate band width and squeeze + band_width = (current_upper - current_lower) / current_middle + band_width_avg = ((upper_band - lower_band) / sma).rolling(window=20).mean().iloc[-1] + + # Detect squeeze (bands tightening) + squeeze_threshold = 0.02 # 2% of price + is_squeeze = band_width < squeeze_threshold + + # Calculate %B (position within bands) + percent_b = (current_price - current_lower) / (current_upper - current_lower) + + # Calculate band position + if current_price > current_upper: + position = "ABOVE_UPPER" + elif current_price < current_lower: + position = "BELOW_LOWER" + elif current_price > current_middle: + position = "UPPER_HALF" + else: + position = "LOWER_HALF" + + return { + 'bb_upper': current_upper, + 'bb_lower': current_lower, + 'bb_middle': current_middle, + 'bb_width': band_width, + 'bb_width_avg': band_width_avg, + 'bb_squeeze': is_squeeze, + 'bb_percent_b': percent_b, + 'bb_position': position, + 'bb_signal': self._get_bollinger_signal(current_price, current_upper, current_lower, current_middle, is_squeeze) + } + + except Exception as e: + self.logger.error(f"āŒ Error calculating Bollinger Bands: {e}") + return {} + + def _get_bollinger_signal(self, price: float, upper: float, lower: float, middle: float, squeeze: bool) -> str: + """Get Bollinger Bands trading signal""" + + # Squeeze breakout strategy + if squeeze: + return "SQUEEZE_WAIT" + + # Mean reversion strategy + if price <= lower: + return "BUY_OVERSOLD" + elif price >= upper: + return "SELL_OVERBOUGHT" + elif price < middle: + return "BELOW_MEAN" + elif price > middle: + return "ABOVE_MEAN" + else: + return "NEUTRAL" + + def _calculate_moving_averages(self, df: pd.DataFrame) -> Dict[str, Any]: + """ + Calculate Moving Averages + + Args: + df: DataFrame with price data + + Returns: + Dictionary with MA values + """ + + try: + close_prices = df['close'] if 'close' in df.columns else df['price'] + + # Calculate primary moving average + if self.ma_type == 'EMA': + ma_value = close_prices.ewm(span=self.ma_period).mean().iloc[-1] + ma_values = close_prices.ewm(span=self.ma_period).mean() + else: # SMA + ma_value = close_prices.rolling(window=self.ma_period).mean().iloc[-1] + ma_values = close_prices.rolling(window=self.ma_period).mean() + + # Calculate fast and slow MAs for crossover + fast_period = max(10, self.ma_period // 2) + slow_period = self.ma_period + + if self.ma_type == 'EMA': + ma_fast = close_prices.ewm(span=fast_period).mean().iloc[-1] + ma_slow = close_prices.ewm(span=slow_period).mean().iloc[-1] + ma_fast_prev = close_prices.ewm(span=fast_period).mean().iloc[-2] + ma_slow_prev = close_prices.ewm(span=slow_period).mean().iloc[-2] + else: + ma_fast = close_prices.rolling(window=fast_period).mean().iloc[-1] + ma_slow = close_prices.rolling(window=slow_period).mean().iloc[-1] + ma_fast_prev = close_prices.rolling(window=fast_period).mean().iloc[-2] + ma_slow_prev = close_prices.rolling(window=slow_period).mean().iloc[-2] + + # Calculate MA slope + ma_slope = (ma_values.iloc[-1] - ma_values.iloc[-5]) / ma_values.iloc[-5] # 5-period slope + + # Determine trend + current_price = close_prices.iloc[-1] + + if current_price > ma_value and ma_slope > 0: + trend = "BULLISH" + elif current_price < ma_value and ma_slope < 0: + trend = "BEARISH" + else: + trend = "SIDEWAYS" + + # Get crossover signal + crossover_signal = self._get_ma_crossover_signal(ma_fast, ma_slow, ma_fast_prev, ma_slow_prev) + + return { + 'ma_value': ma_value, + 'ma_fast': ma_fast, + 'ma_slow': ma_slow, + 'ma_fast_prev': ma_fast_prev, + 'ma_slow_prev': ma_slow_prev, + 'ma_slope': ma_slope, + 'ma_trend': trend, + 'ma_crossover_signal': crossover_signal, + 'ma_signal': self._get_ma_signal(current_price, ma_value, trend) + } + + except Exception as e: + self.logger.error(f"āŒ Error calculating Moving Averages: {e}") + return {} + + def _get_ma_crossover_signal(self, fast: float, slow: float, fast_prev: float, slow_prev: float) -> str: + """Get MA crossover signal""" + + # Golden cross + if fast > slow and fast_prev <= slow_prev: + return "GOLDEN_CROSS" + # Death cross + elif fast < slow and fast_prev >= slow_prev: + return "DEATH_CROSS" + # Above + elif fast > slow: + return "FAST_ABOVE_SLOW" + # Below + elif fast < slow: + return "FAST_BELOW_SLOW" + else: + return "NEUTRAL" + + def _get_ma_signal(self, price: float, ma_value: float, trend: str) -> str: + """Get MA trading signal""" + + if price > ma_value and trend == "BULLISH": + return "BUY_ABOVE_MA" + elif price < ma_value and trend == "BEARISH": + return "SELL_BELOW_MA" + elif price > ma_value and trend == "SIDEWAYS": + return "ABOVE_MA_SIDEWAYS" + elif price < ma_value and trend == "SIDEWAYS": + return "BELOW_MA_SIDEWAYS" + else: + return "NEUTRAL" + + def _calculate_rsi(self, df: pd.DataFrame, period: int = 14) -> Dict[str, Any]: + """ + Calculate RSI (Relative Strength Index) + + Args: + df: DataFrame with price data + period: RSI period + + Returns: + Dictionary with RSI values + """ + + try: + close_prices = df['close'] if 'close' in df.columns else df['price'] + + # Calculate price changes + delta = close_prices.diff() + + # Separate gains and losses + gains = delta.where(delta > 0, 0) + losses = -delta.where(delta < 0, 0) + + # Calculate average gains and losses + avg_gains = gains.rolling(window=period).mean() + avg_losses = losses.rolling(window=period).mean() + + # Calculate RS and RSI + rs = avg_gains / avg_losses + rsi = 100 - (100 / (1 + rs)) + + current_rsi = rsi.iloc[-1] + + # Determine RSI signal + if current_rsi < 30: + rsi_signal = "OVERSOLD" + elif current_rsi > 70: + rsi_signal = "OVERBOUGHT" + elif current_rsi < 50: + rsi_signal = "BEARISH" + else: + rsi_signal = "BULLISH" + + return { + 'rsi': current_rsi, + 'rsi_signal': rsi_signal, + 'rsi_oversold': current_rsi < 30, + 'rsi_overbought': current_rsi > 70 + } + + except Exception as e: + self.logger.error(f"āŒ Error calculating RSI: {e}") + return {} + + def _calculate_additional_indicators(self, df: pd.DataFrame) -> Dict[str, Any]: + """ + Calculate additional technical indicators + + Args: + df: DataFrame with price data + + Returns: + Dictionary with additional indicator values + """ + + try: + indicators = {} + + # MACD + macd_result = self._calculate_macd(df) + indicators.update(macd_result) + + # Volume indicators + if 'volume' in df.columns: + volume_result = self._calculate_volume_indicators(df) + indicators.update(volume_result) + + # Volatility indicators + volatility_result = self._calculate_volatility_indicators(df) + indicators.update(volatility_result) + + # Support/Resistance + support_resistance = self._calculate_support_resistance(df) + indicators.update(support_resistance) + + return indicators + + except Exception as e: + self.logger.error(f"āŒ Error calculating additional indicators: {e}") + return {} + + def _calculate_macd(self, df: pd.DataFrame) -> Dict[str, Any]: + """Calculate MACD indicator""" + + try: + close_prices = df['close'] if 'close' in df.columns else df['price'] + + # Calculate MACD + exp1 = close_prices.ewm(span=12).mean() + exp2 = close_prices.ewm(span=26).mean() + macd = exp1 - exp2 + signal = macd.ewm(span=9).mean() + histogram = macd - signal + + return { + 'macd': macd.iloc[-1], + 'macd_signal': signal.iloc[-1], + 'macd_histogram': histogram.iloc[-1], + 'macd_bullish': macd.iloc[-1] > signal.iloc[-1], + 'macd_cross': self._detect_macd_cross(macd, signal) + } + + except Exception as e: + self.logger.error(f"āŒ Error calculating MACD: {e}") + return {} + + def _detect_macd_cross(self, macd: pd.Series, signal: pd.Series) -> str: + """Detect MACD crossover""" + + if len(macd) < 2 or len(signal) < 2: + return "INSUFFICIENT_DATA" + + current_macd = macd.iloc[-1] + current_signal = signal.iloc[-1] + prev_macd = macd.iloc[-2] + prev_signal = signal.iloc[-2] + + if current_macd > current_signal and prev_macd <= prev_signal: + return "BULLISH_CROSS" + elif current_macd < current_signal and prev_macd >= prev_signal: + return "BEARISH_CROSS" + else: + return "NO_CROSS" + + def _calculate_volume_indicators(self, df: pd.DataFrame) -> Dict[str, Any]: + """Calculate volume-based indicators""" + + try: + volume = df['volume'] + close_prices = df['close'] if 'close' in df.columns else df['price'] + + # Volume moving average + volume_ma = volume.rolling(window=20).mean() + + # Volume ratio + volume_ratio = volume.iloc[-1] / volume_ma.iloc[-1] + + # On-Balance Volume (OBV) + obv = self._calculate_obv(close_prices, volume) + + return { + 'volume_current': volume.iloc[-1], + 'volume_ma': volume_ma.iloc[-1], + 'volume_ratio': volume_ratio, + 'volume_high': volume_ratio > 1.5, + 'obv': obv.iloc[-1], + 'obv_trend': self._get_obv_trend(obv) + } + + except Exception as e: + self.logger.error(f"āŒ Error calculating volume indicators: {e}") + return {} + + def _calculate_obv(self, close_prices: pd.Series, volume: pd.Series) -> pd.Series: + """Calculate On-Balance Volume""" + + price_change = close_prices.diff() + obv = pd.Series(index=close_prices.index, dtype=float) + obv.iloc[0] = volume.iloc[0] + + for i in range(1, len(close_prices)): + if price_change.iloc[i] > 0: + obv.iloc[i] = obv.iloc[i-1] + volume.iloc[i] + elif price_change.iloc[i] < 0: + obv.iloc[i] = obv.iloc[i-1] - volume.iloc[i] + else: + obv.iloc[i] = obv.iloc[i-1] + + return obv + + def _get_obv_trend(self, obv: pd.Series) -> str: + """Get OBV trend""" + + if len(obv) < 5: + return "INSUFFICIENT_DATA" + + recent_obv = obv.iloc[-5:] + trend = np.polyfit(range(len(recent_obv)), recent_obv, 1)[0] + + if trend > 0: + return "BULLISH" + elif trend < 0: + return "BEARISH" + else: + return "NEUTRAL" + + def _calculate_volatility_indicators(self, df: pd.DataFrame) -> Dict[str, Any]: + """Calculate volatility indicators""" + + try: + close_prices = df['close'] if 'close' in df.columns else df['price'] + + # Average True Range (ATR) + if 'high' in df.columns and 'low' in df.columns: + atr = self._calculate_atr(df) + else: + atr = close_prices.rolling(window=14).std() + + # Volatility ratio + short_vol = close_prices.rolling(window=10).std() + long_vol = close_prices.rolling(window=30).std() + volatility_ratio = short_vol.iloc[-1] / long_vol.iloc[-1] + + return { + 'atr': atr.iloc[-1] if hasattr(atr, 'iloc') else atr, + 'volatility_ratio': volatility_ratio, + 'volatility_high': volatility_ratio > 1.2, + 'volatility_low': volatility_ratio < 0.8 + } + + except Exception as e: + self.logger.error(f"āŒ Error calculating volatility indicators: {e}") + return {} + + def _calculate_atr(self, df: pd.DataFrame, period: int = 14) -> pd.Series: + """Calculate Average True Range""" + + high = df['high'] + low = df['low'] + close = df['close'] + + tr1 = high - low + tr2 = abs(high - close.shift()) + tr3 = abs(low - close.shift()) + + true_range = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1) + atr = true_range.rolling(window=period).mean() + + return atr + + def _calculate_support_resistance(self, df: pd.DataFrame) -> Dict[str, Any]: + """Calculate support and resistance levels""" + + try: + close_prices = df['close'] if 'close' in df.columns else df['price'] + + # Simple support/resistance based on recent highs/lows + if 'high' in df.columns and 'low' in df.columns: + high_prices = df['high'] + low_prices = df['low'] + + # Recent resistance (highest high in last 20 periods) + resistance = high_prices.rolling(window=20).max().iloc[-1] + + # Recent support (lowest low in last 20 periods) + support = low_prices.rolling(window=20).min().iloc[-1] + else: + # Use close prices + resistance = close_prices.rolling(window=20).max().iloc[-1] + support = close_prices.rolling(window=20).min().iloc[-1] + + current_price = close_prices.iloc[-1] + + # Calculate distance to support/resistance + resistance_distance = (resistance - current_price) / current_price * 100 + support_distance = (current_price - support) / current_price * 100 + + return { + 'resistance': resistance, + 'support': support, + 'resistance_distance': resistance_distance, + 'support_distance': support_distance, + 'near_resistance': resistance_distance < 2, # Within 2% + 'near_support': support_distance < 2 # Within 2% + } + + except Exception as e: + self.logger.error(f"āŒ Error calculating support/resistance: {e}") + return {} + + def get_combined_signal(self, indicators: Dict[str, Any]) -> Dict[str, Any]: + """ + Generate combined trading signal from all indicators + + Args: + indicators: Dictionary of indicator values + + Returns: + Combined signal analysis + """ + + try: + signals = { + 'buy_signals': 0, + 'sell_signals': 0, + 'neutral_signals': 0, + 'overall_signal': 'NEUTRAL', + 'confidence': 0.0, + 'reasons': [] + } + + # Bollinger Bands signals + bb_signal = indicators.get('bb_signal', 'NEUTRAL') + if bb_signal == 'BUY_OVERSOLD': + signals['buy_signals'] += 1 + signals['reasons'].append('BB: Oversold') + elif bb_signal == 'SELL_OVERBOUGHT': + signals['sell_signals'] += 1 + signals['reasons'].append('BB: Overbought') + + # Moving Average signals + ma_signal = indicators.get('ma_signal', 'NEUTRAL') + if 'BUY' in ma_signal: + signals['buy_signals'] += 1 + signals['reasons'].append('MA: Bullish') + elif 'SELL' in ma_signal: + signals['sell_signals'] += 1 + signals['reasons'].append('MA: Bearish') + + # RSI signals + rsi_signal = indicators.get('rsi_signal', 'NEUTRAL') + if rsi_signal == 'OVERSOLD': + signals['buy_signals'] += 1 + signals['reasons'].append('RSI: Oversold') + elif rsi_signal == 'OVERBOUGHT': + signals['sell_signals'] += 1 + signals['reasons'].append('RSI: Overbought') + + # MACD signals + macd_cross = indicators.get('macd_cross', 'NO_CROSS') + if macd_cross == 'BULLISH_CROSS': + signals['buy_signals'] += 1 + signals['reasons'].append('MACD: Bullish cross') + elif macd_cross == 'BEARISH_CROSS': + signals['sell_signals'] += 1 + signals['reasons'].append('MACD: Bearish cross') + + # Volume confirmation + volume_high = indicators.get('volume_high', False) + if volume_high: + signals['reasons'].append('Volume: High') + + # Determine overall signal + total_signals = signals['buy_signals'] + signals['sell_signals'] + + if total_signals > 0: + if signals['buy_signals'] > signals['sell_signals']: + signals['overall_signal'] = 'BUY' + signals['confidence'] = signals['buy_signals'] / total_signals + elif signals['sell_signals'] > signals['buy_signals']: + signals['overall_signal'] = 'SELL' + signals['confidence'] = signals['sell_signals'] / total_signals + else: + signals['overall_signal'] = 'NEUTRAL' + signals['confidence'] = 0.5 + + return signals + + except Exception as e: + self.logger.error(f"āŒ Error generating combined signal: {e}") + return {'overall_signal': 'NEUTRAL', 'confidence': 0.0, 'reasons': []} + + def get_indicator_summary(self) -> Dict[str, Any]: + """Get summary of current indicators""" + + if not self.indicator_cache: + return {'status': 'No indicators calculated yet'} + + summary = { + 'last_update': self.last_calculation.isoformat() if self.last_calculation else None, + 'bollinger_bands': { + 'upper': self.indicator_cache.get('bb_upper'), + 'lower': self.indicator_cache.get('bb_lower'), + 'signal': self.indicator_cache.get('bb_signal') + }, + 'moving_average': { + 'value': self.indicator_cache.get('ma_value'), + 'trend': self.indicator_cache.get('ma_trend'), + 'signal': self.indicator_cache.get('ma_signal') + }, + 'rsi': { + 'value': self.indicator_cache.get('rsi'), + 'signal': self.indicator_cache.get('rsi_signal') + }, + 'combined_signal': self.get_combined_signal(self.indicator_cache) + } + + return summary \ No newline at end of file diff --git a/trading_bot/bot_core/logger.py b/trading_bot/bot_core/logger.py new file mode 100644 index 0000000..cc198f0 --- /dev/null +++ b/trading_bot/bot_core/logger.py @@ -0,0 +1,263 @@ +""" +Advanced Logging System for Trading Bot +Supports file rotation, structured logging, and multiple log levels +""" + +import logging +import logging.handlers +import os +import sys +from datetime import datetime +from typing import Optional + + +class ColoredFormatter(logging.Formatter): + """Custom formatter with color support for console output""" + + COLORS = { + 'DEBUG': '\033[36m', # Cyan + 'INFO': '\033[32m', # Green + 'WARNING': '\033[33m', # Yellow + 'ERROR': '\033[31m', # Red + 'CRITICAL': '\033[35m', # Magenta + } + + RESET = '\033[0m' + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.use_color = sys.stdout.isatty() # Only use color if outputting to terminal + + def format(self, record): + if self.use_color and record.levelname in self.COLORS: + record.levelname = f"{self.COLORS[record.levelname]}{record.levelname}{self.RESET}" + return super().format(record) + + +def setup_logger( + name: str = "TradingBot", + level: str = "INFO", + log_file: Optional[str] = None, + max_size: int = 10485760, # 10MB + backup_count: int = 5, + console_output: bool = True +) -> logging.Logger: + """ + Setup comprehensive logging with file rotation and console output + + Args: + name: Logger name + level: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL) + log_file: Path to log file (optional) + max_size: Maximum log file size before rotation + backup_count: Number of backup files to keep + console_output: Whether to output to console + + Returns: + Configured logger instance + """ + + # Create logger + logger = logging.getLogger(name) + logger.setLevel(getattr(logging, level.upper())) + + # Clear existing handlers + logger.handlers.clear() + + # Create formatters + console_formatter = ColoredFormatter( + fmt='%(asctime)s | %(levelname)-8s | %(name)s | %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + ) + + file_formatter = logging.Formatter( + fmt='%(asctime)s | %(levelname)-8s | %(name)s | %(funcName)s:%(lineno)d | %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + ) + + # Console handler + if console_output: + console_handler = logging.StreamHandler(sys.stdout) + console_handler.setFormatter(console_formatter) + console_handler.setLevel(getattr(logging, level.upper())) + logger.addHandler(console_handler) + + # File handler with rotation + if log_file: + # Create directory if it doesn't exist + os.makedirs(os.path.dirname(log_file), exist_ok=True) + + file_handler = logging.handlers.RotatingFileHandler( + log_file, + maxBytes=max_size, + backupCount=backup_count, + encoding='utf-8' + ) + file_handler.setFormatter(file_formatter) + file_handler.setLevel(logging.DEBUG) # File gets all levels + logger.addHandler(file_handler) + + # Error handler for critical issues + error_handler = logging.StreamHandler(sys.stderr) + error_handler.setFormatter(file_formatter) + error_handler.setLevel(logging.ERROR) + logger.addHandler(error_handler) + + return logger + + +class TradingLogger: + """ + Enhanced logger wrapper for trading bot with specialized methods + """ + + def __init__(self, logger: logging.Logger): + self.logger = logger + self.trade_count = 0 + self.error_count = 0 + self.session_start = datetime.now() + + def info(self, message: str, **kwargs): + """Log info message with context""" + self.logger.info(self._format_message(message, **kwargs)) + + def warning(self, message: str, **kwargs): + """Log warning message with context""" + self.logger.warning(self._format_message(message, **kwargs)) + + def error(self, message: str, **kwargs): + """Log error message with context""" + self.error_count += 1 + self.logger.error(self._format_message(message, **kwargs)) + + def critical(self, message: str, **kwargs): + """Log critical message with context""" + self.error_count += 1 + self.logger.critical(self._format_message(message, **kwargs)) + + def debug(self, message: str, **kwargs): + """Log debug message with context""" + self.logger.debug(self._format_message(message, **kwargs)) + + def trade(self, action: str, symbol: str, quantity: float, price: float, **kwargs): + """Log trade execution""" + self.trade_count += 1 + message = f"TRADE #{self.trade_count}: {action} {quantity} {symbol} @ {price}" + if kwargs: + message += f" | {kwargs}" + self.logger.info(message) + + def signal(self, signal_type: str, symbol: str, details: dict = None): + """Log trading signal""" + message = f"SIGNAL: {signal_type} for {symbol}" + if details: + message += f" | {details}" + self.logger.info(message) + + def position(self, action: str, symbol: str, details: dict = None): + """Log position changes""" + message = f"POSITION: {action} {symbol}" + if details: + message += f" | {details}" + self.logger.info(message) + + def websocket(self, event: str, details: dict = None): + """Log WebSocket events""" + message = f"WS: {event}" + if details: + message += f" | {details}" + self.logger.debug(message) + + def indicator(self, name: str, symbol: str, values: dict): + """Log indicator calculations""" + message = f"INDICATOR: {name} for {symbol} | {values}" + self.logger.debug(message) + + def risk(self, check: str, result: str, details: dict = None): + """Log risk management events""" + message = f"RISK: {check} -> {result}" + if details: + message += f" | {details}" + self.logger.info(message) + + def performance(self, metrics: dict): + """Log performance metrics""" + message = f"PERFORMANCE: {metrics}" + self.logger.info(message) + + def session_summary(self): + """Log session summary""" + uptime = datetime.now() - self.session_start + self.logger.info(f"SESSION SUMMARY: Uptime: {uptime}, Trades: {self.trade_count}, Errors: {self.error_count}") + + def _format_message(self, message: str, **kwargs) -> str: + """Format message with optional context""" + if kwargs: + context = " | ".join([f"{k}={v}" for k, v in kwargs.items()]) + return f"{message} | {context}" + return message + + +class TradeLogger: + """ + Specialized logger for trade events with structured output + """ + + def __init__(self, logger: logging.Logger): + self.logger = logger + + def log_order_created(self, order_id: str, symbol: str, side: str, quantity: float, price: float = None): + """Log order creation""" + price_str = f" @ {price}" if price else " (MARKET)" + self.logger.info(f"ORDER_CREATED: {order_id} | {side} {quantity} {symbol}{price_str}") + + def log_order_filled(self, order_id: str, symbol: str, side: str, quantity: float, price: float, fee: float = 0): + """Log order fill""" + self.logger.info(f"ORDER_FILLED: {order_id} | {side} {quantity} {symbol} @ {price} | Fee: {fee}") + + def log_order_cancelled(self, order_id: str, reason: str = ""): + """Log order cancellation""" + reason_str = f" | Reason: {reason}" if reason else "" + self.logger.info(f"ORDER_CANCELLED: {order_id}{reason_str}") + + def log_position_opened(self, symbol: str, side: str, quantity: float, entry_price: float): + """Log position opening""" + self.logger.info(f"POSITION_OPENED: {side} {quantity} {symbol} @ {entry_price}") + + def log_position_closed(self, symbol: str, side: str, quantity: float, exit_price: float, pnl: float): + """Log position closing""" + pnl_str = f"+{pnl:.2f}" if pnl > 0 else f"{pnl:.2f}" + self.logger.info(f"POSITION_CLOSED: {side} {quantity} {symbol} @ {exit_price} | PnL: {pnl_str}") + + def log_stop_loss_triggered(self, symbol: str, price: float, loss: float): + """Log stop loss trigger""" + self.logger.warning(f"STOP_LOSS_TRIGGERED: {symbol} @ {price} | Loss: {loss:.2f}") + + def log_take_profit_triggered(self, symbol: str, price: float, profit: float): + """Log take profit trigger""" + self.logger.info(f"TAKE_PROFIT_TRIGGERED: {symbol} @ {price} | Profit: {profit:.2f}") + + def log_risk_check_failed(self, check: str, details: str): + """Log failed risk check""" + self.logger.warning(f"RISK_CHECK_FAILED: {check} | {details}") + + +# Convenience function for getting enhanced logger +def get_trading_logger(name: str = "TradingBot", **kwargs) -> TradingLogger: + """Get enhanced trading logger with specialized methods""" + base_logger = setup_logger(name, **kwargs) + return TradingLogger(base_logger) + + +# Error handling decorator +def log_exceptions(logger: logging.Logger): + """Decorator to log exceptions in functions""" + def decorator(func): + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except Exception as e: + logger.error(f"Exception in {func.__name__}: {str(e)}", exc_info=True) + raise + return wrapper + return decorator \ No newline at end of file diff --git a/trading_bot/bot_core/order_manager.py b/trading_bot/bot_core/order_manager.py new file mode 100644 index 0000000..64d83bc --- /dev/null +++ b/trading_bot/bot_core/order_manager.py @@ -0,0 +1,544 @@ +""" +Order Management System +Handles order creation, execution, and tracking +""" + +import asyncio +import time +import uuid +from datetime import datetime, timedelta +from typing import Dict, List, Optional, Any +from dataclasses import dataclass +from enum import Enum + +import ccxt + +from .config import Config +from .logger import TradingLogger + + +class OrderStatus(Enum): + """Order status enumeration""" + PENDING = "pending" + SUBMITTED = "submitted" + OPEN = "open" + PARTIALLY_FILLED = "partially_filled" + FILLED = "filled" + CANCELLED = "cancelled" + REJECTED = "rejected" + EXPIRED = "expired" + + +class OrderType(Enum): + """Order type enumeration""" + MARKET = "market" + LIMIT = "limit" + STOP = "stop" + STOP_LIMIT = "stop_limit" + + +class OrderSide(Enum): + """Order side enumeration""" + BUY = "buy" + SELL = "sell" + + +@dataclass +class Order: + """Order data structure""" + id: str + symbol: str + side: OrderSide + type: OrderType + quantity: float + price: Optional[float] = None + stop_price: Optional[float] = None + status: OrderStatus = OrderStatus.PENDING + filled_quantity: float = 0.0 + average_price: float = 0.0 + fees: float = 0.0 + created_at: datetime = None + updated_at: datetime = None + exchange_order_id: Optional[str] = None + client_order_id: Optional[str] = None + + def __post_init__(self): + if self.created_at is None: + self.created_at = datetime.now() + if self.updated_at is None: + self.updated_at = datetime.now() + if self.client_order_id is None: + self.client_order_id = f"bot_{int(time.time() * 1000)}" + + +class OrderManager: + """ + Manages order creation, execution, and tracking + """ + + def __init__(self, config: Config, logger: TradingLogger, exchange: ccxt.Exchange): + self.config = config + self.logger = logger + self.exchange = exchange + + # Order tracking + self.orders: Dict[str, Order] = {} + self.order_history: List[Order] = [] + + # Statistics + self.orders_created = 0 + self.orders_filled = 0 + self.orders_cancelled = 0 + self.orders_rejected = 0 + + # Rate limiting + self.last_order_time = 0 + self.min_order_interval = 1.0 # Minimum seconds between orders + + # Market data + self.current_price = 0.0 + self.bid_price = 0.0 + self.ask_price = 0.0 + self.spread = 0.0 + + self.logger.info("šŸ’¼ Order Manager initialized") + + async def place_buy_order(self, symbol: str, quantity: float, price: float = None) -> Optional[Order]: + """ + Place a buy order + + Args: + symbol: Trading symbol + quantity: Order quantity + price: Order price (None for market order) + + Returns: + Order object if successful, None otherwise + """ + + try: + # Validate order parameters + if not self._validate_order_params(symbol, quantity, price): + return None + + # Check rate limiting + if not await self._check_rate_limit(): + return None + + # Create order object + order = Order( + id=str(uuid.uuid4()), + symbol=symbol, + side=OrderSide.BUY, + type=OrderType.LIMIT if price else OrderType.MARKET, + quantity=quantity, + price=price + ) + + # Place order based on trading mode + if self.config.is_paper_trading(): + await self._place_paper_order(order) + else: + await self._place_live_order(order) + + # Track order + self.orders[order.id] = order + self.orders_created += 1 + + self.logger.trade("BUY", symbol, quantity, price or self.current_price) + + return order + + except Exception as e: + self.logger.error(f"āŒ Error placing buy order: {e}") + return None + + async def place_sell_order(self, symbol: str, quantity: float, price: float = None) -> Optional[Order]: + """ + Place a sell order + + Args: + symbol: Trading symbol + quantity: Order quantity + price: Order price (None for market order) + + Returns: + Order object if successful, None otherwise + """ + + try: + # Validate order parameters + if not self._validate_order_params(symbol, quantity, price): + return None + + # Check rate limiting + if not await self._check_rate_limit(): + return None + + # Create order object + order = Order( + id=str(uuid.uuid4()), + symbol=symbol, + side=OrderSide.SELL, + type=OrderType.LIMIT if price else OrderType.MARKET, + quantity=quantity, + price=price + ) + + # Place order based on trading mode + if self.config.is_paper_trading(): + await self._place_paper_order(order) + else: + await self._place_live_order(order) + + # Track order + self.orders[order.id] = order + self.orders_created += 1 + + self.logger.trade("SELL", symbol, quantity, price or self.current_price) + + return order + + except Exception as e: + self.logger.error(f"āŒ Error placing sell order: {e}") + return None + + async def cancel_order(self, order_id: str) -> bool: + """ + Cancel an order + + Args: + order_id: Order ID to cancel + + Returns: + True if successful, False otherwise + """ + + try: + if order_id not in self.orders: + self.logger.warning(f"āš ļø Order {order_id} not found") + return False + + order = self.orders[order_id] + + # Can only cancel open orders + if order.status not in [OrderStatus.OPEN, OrderStatus.PARTIALLY_FILLED]: + self.logger.warning(f"āš ļø Cannot cancel order {order_id} with status {order.status}") + return False + + # Cancel order based on trading mode + if self.config.is_paper_trading(): + await self._cancel_paper_order(order) + else: + await self._cancel_live_order(order) + + order.status = OrderStatus.CANCELLED + order.updated_at = datetime.now() + + # Move to history + self.order_history.append(order) + del self.orders[order_id] + + self.orders_cancelled += 1 + self.logger.info(f"āœ… Order {order_id} cancelled") + + return True + + except Exception as e: + self.logger.error(f"āŒ Error cancelling order {order_id}: {e}") + return False + + async def cancel_all_orders(self) -> int: + """ + Cancel all open orders + + Returns: + Number of orders cancelled + """ + + try: + cancelled_count = 0 + order_ids = list(self.orders.keys()) + + for order_id in order_ids: + if await self.cancel_order(order_id): + cancelled_count += 1 + + self.logger.info(f"āœ… Cancelled {cancelled_count} orders") + return cancelled_count + + except Exception as e: + self.logger.error(f"āŒ Error cancelling all orders: {e}") + return 0 + + async def get_open_orders(self) -> List[Order]: + """Get all open orders""" + + open_orders = [] + + for order in self.orders.values(): + if order.status in [OrderStatus.OPEN, OrderStatus.PARTIALLY_FILLED]: + open_orders.append(order) + + return open_orders + + async def get_order_status(self, order_id: str) -> Optional[OrderStatus]: + """Get order status""" + + if order_id in self.orders: + return self.orders[order_id].status + + # Check order history + for order in self.order_history: + if order.id == order_id: + return order.status + + return None + + async def update_market_data(self, market_data: Dict[str, Any]): + """Update market data for order processing""" + + try: + self.current_price = market_data.get('price', self.current_price) + + # Update bid/ask from depth data + if market_data.get('type') == 'depth': + bids = market_data.get('bids', []) + asks = market_data.get('asks', []) + + if bids: + self.bid_price = bids[0][0] + if asks: + self.ask_price = asks[0][0] + + if self.bid_price and self.ask_price: + self.spread = self.ask_price - self.bid_price + + # Check for order fills in paper trading + if self.config.is_paper_trading(): + await self._check_paper_order_fills() + + except Exception as e: + self.logger.error(f"āŒ Error updating market data: {e}") + + async def check_stuck_orders(self): + """Check for stuck orders and handle them""" + + try: + current_time = datetime.now() + stuck_threshold = timedelta(minutes=10) # 10 minutes + + for order_id, order in list(self.orders.items()): + if current_time - order.created_at > stuck_threshold: + self.logger.warning(f"āš ļø Order {order_id} appears stuck, cancelling...") + await self.cancel_order(order_id) + + except Exception as e: + self.logger.error(f"āŒ Error checking stuck orders: {e}") + + async def _place_live_order(self, order: Order): + """Place order on live exchange""" + + try: + # Prepare order parameters + order_params = { + 'symbol': order.symbol, + 'type': order.type.value, + 'side': order.side.value, + 'amount': order.quantity, + 'params': { + 'timeInForce': 'GTC', + 'newClientOrderId': order.client_order_id + } + } + + # Add price for limit orders + if order.type == OrderType.LIMIT: + order_params['price'] = order.price + + # Place order + result = await self.exchange.create_order(**order_params) + + # Update order with exchange response + order.exchange_order_id = result.get('id') + order.status = OrderStatus.SUBMITTED + order.updated_at = datetime.now() + + self.logger.info(f"āœ… Live order placed: {order.exchange_order_id}") + + except Exception as e: + order.status = OrderStatus.REJECTED + order.updated_at = datetime.now() + self.orders_rejected += 1 + self.logger.error(f"āŒ Live order placement failed: {e}") + raise + + async def _place_paper_order(self, order: Order): + """Place paper trading order""" + + try: + # Simulate order placement + await asyncio.sleep(0.1) # Simulate network delay + + order.status = OrderStatus.OPEN + order.updated_at = datetime.now() + + self.logger.info(f"šŸ“„ Paper order placed: {order.id}") + + except Exception as e: + order.status = OrderStatus.REJECTED + order.updated_at = datetime.now() + self.orders_rejected += 1 + self.logger.error(f"āŒ Paper order placement failed: {e}") + raise + + async def _cancel_live_order(self, order: Order): + """Cancel live order""" + + try: + if order.exchange_order_id: + await self.exchange.cancel_order(order.exchange_order_id, order.symbol) + self.logger.info(f"āœ… Live order cancelled: {order.exchange_order_id}") + + except Exception as e: + self.logger.error(f"āŒ Live order cancellation failed: {e}") + raise + + async def _cancel_paper_order(self, order: Order): + """Cancel paper trading order""" + + try: + # Simulate cancellation + await asyncio.sleep(0.1) + self.logger.info(f"šŸ“„ Paper order cancelled: {order.id}") + + except Exception as e: + self.logger.error(f"āŒ Paper order cancellation failed: {e}") + raise + + async def _check_paper_order_fills(self): + """Check for paper order fills based on current market price""" + + try: + for order_id, order in list(self.orders.items()): + if order.status != OrderStatus.OPEN: + continue + + # Check if order should be filled + should_fill = False + + if order.type == OrderType.MARKET: + should_fill = True + elif order.type == OrderType.LIMIT: + if order.side == OrderSide.BUY and self.current_price <= order.price: + should_fill = True + elif order.side == OrderSide.SELL and self.current_price >= order.price: + should_fill = True + + if should_fill: + await self._fill_paper_order(order) + + except Exception as e: + self.logger.error(f"āŒ Error checking paper order fills: {e}") + + async def _fill_paper_order(self, order: Order): + """Fill a paper trading order""" + + try: + # Simulate order fill + fill_price = order.price if order.type == OrderType.LIMIT else self.current_price + + order.status = OrderStatus.FILLED + order.filled_quantity = order.quantity + order.average_price = fill_price + order.fees = order.quantity * fill_price * 0.001 # 0.1% fee + order.updated_at = datetime.now() + + # Move to history + self.order_history.append(order) + del self.orders[order.id] + + self.orders_filled += 1 + self.logger.info(f"šŸ“„ Paper order filled: {order.id} at {fill_price}") + + except Exception as e: + self.logger.error(f"āŒ Error filling paper order: {e}") + + def _validate_order_params(self, symbol: str, quantity: float, price: float = None) -> bool: + """Validate order parameters""" + + try: + # Basic validation + if not symbol: + self.logger.error("āŒ Symbol is required") + return False + + if quantity <= 0: + self.logger.error("āŒ Quantity must be positive") + return False + + if price is not None and price <= 0: + self.logger.error("āŒ Price must be positive") + return False + + # Check minimum order size + min_quantity = 0.001 # Minimum for most crypto pairs + if quantity < min_quantity: + self.logger.error(f"āŒ Quantity below minimum: {min_quantity}") + return False + + # Check price deviation (for limit orders) + if price is not None and self.current_price > 0: + price_deviation = abs(price - self.current_price) / self.current_price + if price_deviation > 0.1: # 10% deviation + self.logger.warning(f"āš ļø Large price deviation: {price_deviation:.2%}") + + return True + + except Exception as e: + self.logger.error(f"āŒ Error validating order parameters: {e}") + return False + + async def _check_rate_limit(self) -> bool: + """Check rate limiting for orders""" + + try: + current_time = time.time() + + if current_time - self.last_order_time < self.min_order_interval: + wait_time = self.min_order_interval - (current_time - self.last_order_time) + self.logger.warning(f"ā±ļø Rate limit: waiting {wait_time:.2f}s") + await asyncio.sleep(wait_time) + + self.last_order_time = time.time() + return True + + except Exception as e: + self.logger.error(f"āŒ Error checking rate limit: {e}") + return False + + def get_statistics(self) -> Dict[str, Any]: + """Get order management statistics""" + + return { + 'orders_created': self.orders_created, + 'orders_filled': self.orders_filled, + 'orders_cancelled': self.orders_cancelled, + 'orders_rejected': self.orders_rejected, + 'open_orders': len(self.orders), + 'order_history': len(self.order_history), + 'fill_rate': self.orders_filled / max(1, self.orders_created) * 100, + 'current_price': self.current_price, + 'spread': self.spread + } + + def get_order_book_summary(self) -> Dict[str, Any]: + """Get order book summary""" + + return { + 'bid_price': self.bid_price, + 'ask_price': self.ask_price, + 'spread': self.spread, + 'spread_percent': (self.spread / self.current_price * 100) if self.current_price > 0 else 0, + 'mid_price': (self.bid_price + self.ask_price) / 2 if self.bid_price and self.ask_price else 0 + } \ No newline at end of file diff --git a/trading_bot/bot_core/position_manager.py b/trading_bot/bot_core/position_manager.py new file mode 100644 index 0000000..ff15a66 --- /dev/null +++ b/trading_bot/bot_core/position_manager.py @@ -0,0 +1,469 @@ +""" +Position Manager +Tracks positions and calculates PnL +""" + +from datetime import datetime +from typing import Dict, List, Optional, Any +from dataclasses import dataclass +import json + +from .config import Config +from .logger import TradingLogger + + +@dataclass +class Position: + """Position data structure""" + id: str + symbol: str + side: str # 'long' or 'short' + quantity: float + entry_price: float + current_price: float = 0.0 + unrealized_pnl: float = 0.0 + realized_pnl: float = 0.0 + fees: float = 0.0 + created_at: datetime = None + updated_at: datetime = None + + def __post_init__(self): + if self.created_at is None: + self.created_at = datetime.now() + if self.updated_at is None: + self.updated_at = datetime.now() + + +class PositionManager: + """Manages trading positions and PnL calculation""" + + def __init__(self, config: Config, logger: TradingLogger, exchange): + self.config = config + self.logger = logger + self.exchange = exchange + + # Position tracking + self.positions: Dict[str, Position] = {} + self.position_history: List[Position] = [] + + # PnL tracking + self.total_realized_pnl = 0.0 + self.total_unrealized_pnl = 0.0 + self.total_fees = 0.0 + + # Statistics + self.total_positions = 0 + self.winning_positions = 0 + self.losing_positions = 0 + + self.logger.info("šŸ“Š Position Manager initialized") + + async def open_position(self, symbol: str, side: str, quantity: float, entry_price: float) -> Position: + """Open a new position""" + + try: + position = Position( + id=f"{symbol}_{side}_{int(datetime.now().timestamp())}", + symbol=symbol, + side=side, + quantity=quantity, + entry_price=entry_price, + current_price=entry_price + ) + + self.positions[position.id] = position + self.total_positions += 1 + + self.logger.position("OPENED", symbol, { + "side": side, + "quantity": quantity, + "entry_price": entry_price, + "position_id": position.id + }) + + return position + + except Exception as e: + self.logger.error(f"āŒ Error opening position: {e}") + raise + + async def close_position(self, position: Position, reason: str = "") -> float: + """Close a position and calculate realized PnL""" + + try: + # Calculate realized PnL + if position.side == 'long': + realized_pnl = (position.current_price - position.entry_price) * position.quantity + else: # short + realized_pnl = (position.entry_price - position.current_price) * position.quantity + + # Account for fees + realized_pnl -= position.fees + + # Update position + position.realized_pnl = realized_pnl + position.updated_at = datetime.now() + + # Update statistics + self.total_realized_pnl += realized_pnl + self.total_fees += position.fees + + if realized_pnl > 0: + self.winning_positions += 1 + else: + self.losing_positions += 1 + + # Move to history + self.position_history.append(position) + + # Remove from active positions + if position.id in self.positions: + del self.positions[position.id] + + self.logger.position("CLOSED", position.symbol, { + "side": position.side, + "quantity": position.quantity, + "entry_price": position.entry_price, + "exit_price": position.current_price, + "realized_pnl": realized_pnl, + "reason": reason + }) + + return realized_pnl + + except Exception as e: + self.logger.error(f"āŒ Error closing position: {e}") + return 0.0 + + async def update_positions(self, current_prices: Dict[str, float] = None): + """Update all positions with current prices""" + + try: + for position in self.positions.values(): + if current_prices and position.symbol in current_prices: + position.current_price = current_prices[position.symbol] + + # Calculate unrealized PnL + if position.side == 'long': + position.unrealized_pnl = (position.current_price - position.entry_price) * position.quantity + else: # short + position.unrealized_pnl = (position.entry_price - position.current_price) * position.quantity + + # Account for fees + position.unrealized_pnl -= position.fees + + position.updated_at = datetime.now() + + # Update total unrealized PnL + self.total_unrealized_pnl = sum(pos.unrealized_pnl for pos in self.positions.values()) + + except Exception as e: + self.logger.error(f"āŒ Error updating positions: {e}") + + async def get_positions(self) -> List[Position]: + """Get all active positions""" + return list(self.positions.values()) + + async def get_position_by_symbol(self, symbol: str) -> Optional[Position]: + """Get position by symbol""" + for position in self.positions.values(): + if position.symbol == symbol: + return position + return None + + async def close_all_positions(self, reason: str = "Emergency close"): + """Close all positions""" + + try: + positions_to_close = list(self.positions.values()) + + for position in positions_to_close: + await self.close_position(position, reason) + + self.logger.info(f"āœ… Closed {len(positions_to_close)} positions") + + except Exception as e: + self.logger.error(f"āŒ Error closing all positions: {e}") + + def get_portfolio_value(self) -> float: + """Get current portfolio value""" + return self.total_realized_pnl + self.total_unrealized_pnl + + def get_position_summary(self) -> Dict[str, Any]: + """Get position summary""" + + total_positions = self.total_positions + win_rate = (self.winning_positions / max(1, total_positions)) * 100 + + return { + 'active_positions': len(self.positions), + 'total_positions': total_positions, + 'winning_positions': self.winning_positions, + 'losing_positions': self.losing_positions, + 'win_rate': win_rate, + 'total_realized_pnl': self.total_realized_pnl, + 'total_unrealized_pnl': self.total_unrealized_pnl, + 'total_fees': self.total_fees, + 'portfolio_value': self.get_portfolio_value() + } + + def get_position_details(self) -> List[Dict[str, Any]]: + """Get detailed position information""" + + details = [] + + for position in self.positions.values(): + details.append({ + 'id': position.id, + 'symbol': position.symbol, + 'side': position.side, + 'quantity': position.quantity, + 'entry_price': position.entry_price, + 'current_price': position.current_price, + 'unrealized_pnl': position.unrealized_pnl, + 'pnl_percent': (position.unrealized_pnl / (position.entry_price * position.quantity)) * 100, + 'created_at': position.created_at.isoformat(), + 'age': str(datetime.now() - position.created_at) + }) + + return details + + def save_positions_to_file(self, filename: str = "positions.json"): + """Save positions to file""" + + try: + data = { + 'active_positions': self.get_position_details(), + 'summary': self.get_position_summary(), + 'timestamp': datetime.now().isoformat() + } + + with open(filename, 'w') as f: + json.dump(data, f, indent=2) + + self.logger.info(f"āœ… Positions saved to {filename}") + + except Exception as e: + self.logger.error(f"āŒ Error saving positions: {e}") + + +class PerformanceTracker: + """Tracks trading performance metrics""" + + def __init__(self, config: Config, logger: TradingLogger): + self.config = config + self.logger = logger + + # Performance metrics + self.total_trades = 0 + self.winning_trades = 0 + self.losing_trades = 0 + self.total_pnl = 0.0 + self.total_volume = 0.0 + self.total_fees = 0.0 + + # Daily metrics + self.daily_pnl = 0.0 + self.daily_trades = 0 + self.daily_volume = 0.0 + + # Risk metrics + self.max_drawdown = 0.0 + self.current_drawdown = 0.0 + self.peak_portfolio_value = 0.0 + + # Time tracking + self.session_start = datetime.now() + self.last_trade_time = None + + self.logger.info("šŸ“ˆ Performance Tracker initialized") + + async def record_trade(self, symbol: str, side: str, quantity: float, entry_price: float, + exit_price: float, fees: float = 0.0): + """Record a completed trade""" + + try: + # Calculate trade PnL + if side == 'long': + trade_pnl = (exit_price - entry_price) * quantity + else: # short + trade_pnl = (entry_price - exit_price) * quantity + + # Account for fees + trade_pnl -= fees + + # Update metrics + self.total_trades += 1 + self.total_pnl += trade_pnl + self.total_volume += quantity * entry_price + self.total_fees += fees + + # Update daily metrics + self.daily_trades += 1 + self.daily_pnl += trade_pnl + self.daily_volume += quantity * entry_price + + # Update win/loss counts + if trade_pnl > 0: + self.winning_trades += 1 + else: + self.losing_trades += 1 + + self.last_trade_time = datetime.now() + + # Log trade + self.logger.performance({ + 'trade_pnl': trade_pnl, + 'total_pnl': self.total_pnl, + 'win_rate': self.get_win_rate(), + 'total_trades': self.total_trades + }) + + except Exception as e: + self.logger.error(f"āŒ Error recording trade: {e}") + + async def update_metrics(self): + """Update performance metrics""" + + try: + # Calculate current metrics + metrics = self.get_performance_metrics() + + # Update drawdown + if self.total_pnl > self.peak_portfolio_value: + self.peak_portfolio_value = self.total_pnl + + if self.peak_portfolio_value > 0: + self.current_drawdown = ((self.peak_portfolio_value - self.total_pnl) / self.peak_portfolio_value) * 100 + self.max_drawdown = max(self.max_drawdown, self.current_drawdown) + + # Log metrics periodically + if self.total_trades > 0 and self.total_trades % 10 == 0: + self.logger.performance(metrics) + + except Exception as e: + self.logger.error(f"āŒ Error updating metrics: {e}") + + def get_performance_metrics(self) -> Dict[str, Any]: + """Get comprehensive performance metrics""" + + uptime = datetime.now() - self.session_start + + return { + 'total_trades': self.total_trades, + 'winning_trades': self.winning_trades, + 'losing_trades': self.losing_trades, + 'win_rate': self.get_win_rate(), + 'total_pnl': self.total_pnl, + 'average_pnl_per_trade': self.total_pnl / max(1, self.total_trades), + 'total_volume': self.total_volume, + 'total_fees': self.total_fees, + 'profit_factor': self.get_profit_factor(), + 'sharpe_ratio': self.get_sharpe_ratio(), + 'max_drawdown': self.max_drawdown, + 'current_drawdown': self.current_drawdown, + 'daily_pnl': self.daily_pnl, + 'daily_trades': self.daily_trades, + 'session_uptime': str(uptime), + 'last_trade_time': self.last_trade_time.isoformat() if self.last_trade_time else None + } + + def get_win_rate(self) -> float: + """Calculate win rate percentage""" + if self.total_trades == 0: + return 0.0 + return (self.winning_trades / self.total_trades) * 100 + + def get_profit_factor(self) -> float: + """Calculate profit factor""" + if self.losing_trades == 0: + return float('inf') if self.winning_trades > 0 else 0.0 + + # This is a simplified profit factor calculation + # In practice, you'd track gross profit and gross loss separately + if self.total_pnl > 0: + return abs(self.total_pnl) / max(1, self.total_fees) + else: + return 0.0 + + def get_sharpe_ratio(self) -> float: + """Calculate Sharpe ratio (simplified)""" + # This is a simplified calculation + # In practice, you'd need returns over time to calculate proper Sharpe ratio + if self.total_trades < 2: + return 0.0 + + avg_return = self.total_pnl / self.total_trades + # Simplified risk-free rate assumption + risk_free_rate = 0.02 # 2% annually + + # Simplified standard deviation calculation + if self.total_trades > 0: + return (avg_return - risk_free_rate) / max(0.01, abs(avg_return)) + + return 0.0 + + async def get_daily_pnl(self) -> float: + """Get daily PnL""" + return self.daily_pnl + + async def reset_daily_metrics(self): + """Reset daily metrics (called at start of new day)""" + self.daily_pnl = 0.0 + self.daily_trades = 0 + self.daily_volume = 0.0 + self.logger.info("šŸ“… Daily metrics reset") + + async def save_session_data(self): + """Save session data to file""" + + try: + data = { + 'performance_metrics': self.get_performance_metrics(), + 'session_start': self.session_start.isoformat(), + 'session_end': datetime.now().isoformat() + } + + filename = f"session_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" + + with open(filename, 'w') as f: + json.dump(data, f, indent=2) + + self.logger.info(f"āœ… Session data saved to {filename}") + + except Exception as e: + self.logger.error(f"āŒ Error saving session data: {e}") + + def get_summary_report(self) -> str: + """Generate a summary report""" + + metrics = self.get_performance_metrics() + + report = f""" +šŸ“Š TRADING SESSION SUMMARY +======================== + +šŸ”¢ Trade Statistics: + Total Trades: {metrics['total_trades']} + Winning Trades: {metrics['winning_trades']} + Losing Trades: {metrics['losing_trades']} + Win Rate: {metrics['win_rate']:.2f}% + +šŸ’° Financial Performance: + Total PnL: ${metrics['total_pnl']:.2f} + Average PnL per Trade: ${metrics['average_pnl_per_trade']:.2f} + Total Volume: ${metrics['total_volume']:.2f} + Total Fees: ${metrics['total_fees']:.2f} + +šŸ“ˆ Risk Metrics: + Max Drawdown: {metrics['max_drawdown']:.2f}% + Current Drawdown: {metrics['current_drawdown']:.2f}% + Profit Factor: {metrics['profit_factor']:.2f} + Sharpe Ratio: {metrics['sharpe_ratio']:.2f} + +ā° Session Info: + Uptime: {metrics['session_uptime']} + Last Trade: {metrics['last_trade_time'] or 'None'} +""" + + return report \ No newline at end of file diff --git a/trading_bot/bot_core/risk_manager.py b/trading_bot/bot_core/risk_manager.py new file mode 100644 index 0000000..b3e783b --- /dev/null +++ b/trading_bot/bot_core/risk_manager.py @@ -0,0 +1,405 @@ +""" +Risk Management System +Handles position sizing, stop-loss, take-profit, and risk limits +""" + +import asyncio +from datetime import datetime, timedelta +from typing import Dict, List, Optional, Tuple, Any +from dataclasses import dataclass + +from .config import Config +from .logger import TradingLogger + + +@dataclass +class RiskLimits: + """Risk limits configuration""" + max_position_size: float + max_daily_loss: float + max_portfolio_risk: float + stop_loss_percent: float + take_profit_percent: float + max_drawdown: float = 20.0 + + +class RiskManager: + """ + Manages risk limits and position sizing + """ + + def __init__(self, config: Config, logger: TradingLogger): + self.config = config + self.logger = logger + + # Risk limits + self.risk_limits = RiskLimits( + max_position_size=config.POSITION_SIZE, + max_daily_loss=config.MAX_DAILY_LOSS, + max_portfolio_risk=config.MAX_PORTFOLIO_RISK, + stop_loss_percent=config.STOP_LOSS_PERCENT, + take_profit_percent=config.TAKE_PROFIT_PERCENT + ) + + # Daily tracking + self.daily_pnl = 0.0 + self.daily_trades = 0 + self.daily_losses = 0 + self.last_reset = datetime.now().date() + + # Portfolio tracking + self.total_exposure = 0.0 + self.max_drawdown = 0.0 + self.peak_portfolio_value = 0.0 + + # Emergency states + self.trading_halted = False + self.halt_reason = "" + + self.logger.info(f"šŸ›”ļø Risk Manager initialized with SL: {self.risk_limits.stop_loss_percent}%, TP: {self.risk_limits.take_profit_percent}%") + + async def check_signal_risk(self, signal) -> bool: + """ + Check if signal passes risk checks + + Args: + signal: Trading signal to check + + Returns: + True if signal passes risk checks, False otherwise + """ + + try: + # Check if trading is halted + if self.trading_halted: + self.logger.risk("SIGNAL_CHECK", "REJECTED", {"reason": self.halt_reason}) + return False + + # Check daily loss limit + if self.daily_pnl <= -self.risk_limits.max_daily_loss: + self.logger.risk("DAILY_LOSS_CHECK", "REJECTED", {"daily_pnl": self.daily_pnl}) + return False + + # Check portfolio risk + if self.total_exposure >= self.risk_limits.max_portfolio_risk: + self.logger.risk("PORTFOLIO_RISK_CHECK", "REJECTED", {"exposure": self.total_exposure}) + return False + + # Check maximum drawdown + if self.max_drawdown >= self.risk_limits.max_drawdown: + self.logger.risk("DRAWDOWN_CHECK", "REJECTED", {"drawdown": self.max_drawdown}) + return False + + # Check signal strength + if signal.strength < 0.5: + self.logger.risk("SIGNAL_STRENGTH_CHECK", "REJECTED", {"strength": signal.strength}) + return False + + self.logger.risk("SIGNAL_CHECK", "PASSED", {"signal_type": signal.signal_type}) + return True + + except Exception as e: + self.logger.error(f"āŒ Error in signal risk check: {e}") + return False + + async def calculate_position_size(self, signal) -> float: + """ + Calculate position size based on risk parameters + + Args: + signal: Trading signal + + Returns: + Position size + """ + + try: + # Base position size + base_size = self.risk_limits.max_position_size + + # Adjust based on signal strength + strength_multiplier = max(0.5, min(1.0, signal.strength)) + + # Adjust based on volatility (if available) + volatility_multiplier = 1.0 + if hasattr(signal, 'indicators') and signal.indicators: + volatility_ratio = signal.indicators.get('volatility_ratio', 1.0) + if volatility_ratio > 1.5: # High volatility + volatility_multiplier = 0.7 + elif volatility_ratio < 0.8: # Low volatility + volatility_multiplier = 1.2 + + # Calculate final position size + position_size = base_size * strength_multiplier * volatility_multiplier + + # Ensure minimum viable size + min_size = 0.001 + position_size = max(min_size, position_size) + + # Ensure doesn't exceed maximum + position_size = min(position_size, self.risk_limits.max_position_size) + + self.logger.risk("POSITION_SIZE_CALC", "CALCULATED", { + "base_size": base_size, + "strength_multiplier": strength_multiplier, + "volatility_multiplier": volatility_multiplier, + "final_size": position_size + }) + + return position_size + + except Exception as e: + self.logger.error(f"āŒ Error calculating position size: {e}") + return self.risk_limits.max_position_size * 0.5 # Conservative fallback + + async def check_position_exit(self, position, current_price: float) -> Tuple[bool, str]: + """ + Check if position should be closed + + Args: + position: Current position + current_price: Current market price + + Returns: + Tuple of (should_close, reason) + """ + + try: + entry_price = position.entry_price + side = position.side + + # Calculate PnL percentage + if side == 'long': + pnl_percent = ((current_price - entry_price) / entry_price) * 100 + else: # short + pnl_percent = ((entry_price - current_price) / entry_price) * 100 + + # Check stop loss + if pnl_percent <= -self.risk_limits.stop_loss_percent: + return True, f"STOP_LOSS_TRIGGERED: {pnl_percent:.2f}%" + + # Check take profit + if pnl_percent >= self.risk_limits.take_profit_percent: + return True, f"TAKE_PROFIT_TRIGGERED: {pnl_percent:.2f}%" + + # Check time-based exit (optional) + if hasattr(position, 'created_at'): + position_age = datetime.now() - position.created_at + if position_age > timedelta(hours=24): # 24 hour max hold + return True, f"TIME_EXIT: Position held for {position_age}" + + return False, "" + + except Exception as e: + self.logger.error(f"āŒ Error checking position exit: {e}") + return False, "" + + async def update_daily_metrics(self, trade_pnl: float, trade_result: str): + """ + Update daily risk metrics + + Args: + trade_pnl: Trade PnL + trade_result: Trade result ('win' or 'loss') + """ + + try: + # Reset daily metrics if new day + current_date = datetime.now().date() + if current_date != self.last_reset: + self.daily_pnl = 0.0 + self.daily_trades = 0 + self.daily_losses = 0 + self.last_reset = current_date + self.logger.info("šŸ“… Daily metrics reset") + + # Update metrics + self.daily_pnl += trade_pnl + self.daily_trades += 1 + + if trade_result == 'loss': + self.daily_losses += 1 + + # Check if daily loss limit reached + if self.daily_pnl <= -self.risk_limits.max_daily_loss: + await self.halt_trading("Daily loss limit exceeded") + + # Log daily metrics + self.logger.risk("DAILY_METRICS", "UPDATED", { + "daily_pnl": self.daily_pnl, + "daily_trades": self.daily_trades, + "daily_losses": self.daily_losses, + "win_rate": ((self.daily_trades - self.daily_losses) / max(1, self.daily_trades)) * 100 + }) + + except Exception as e: + self.logger.error(f"āŒ Error updating daily metrics: {e}") + + async def update_portfolio_metrics(self, portfolio_value: float): + """ + Update portfolio-level risk metrics + + Args: + portfolio_value: Current portfolio value + """ + + try: + # Update peak portfolio value + if portfolio_value > self.peak_portfolio_value: + self.peak_portfolio_value = portfolio_value + + # Calculate current drawdown + if self.peak_portfolio_value > 0: + current_drawdown = ((self.peak_portfolio_value - portfolio_value) / self.peak_portfolio_value) * 100 + self.max_drawdown = max(self.max_drawdown, current_drawdown) + + # Check maximum drawdown limit + if self.max_drawdown >= self.risk_limits.max_drawdown: + await self.halt_trading(f"Maximum drawdown exceeded: {self.max_drawdown:.2f}%") + + self.logger.risk("PORTFOLIO_METRICS", "UPDATED", { + "portfolio_value": portfolio_value, + "peak_value": self.peak_portfolio_value, + "current_drawdown": current_drawdown if self.peak_portfolio_value > 0 else 0, + "max_drawdown": self.max_drawdown + }) + + except Exception as e: + self.logger.error(f"āŒ Error updating portfolio metrics: {e}") + + async def halt_trading(self, reason: str): + """ + Halt trading due to risk limits + + Args: + reason: Reason for halting trading + """ + + try: + self.trading_halted = True + self.halt_reason = reason + + self.logger.critical(f"🚨 TRADING HALTED: {reason}") + + # Could add additional emergency actions here: + # - Send notifications + # - Close all positions + # - Cancel all orders + + except Exception as e: + self.logger.error(f"āŒ Error halting trading: {e}") + + async def resume_trading(self): + """Resume trading after manual review""" + + try: + self.trading_halted = False + self.halt_reason = "" + + self.logger.info("āœ… Trading resumed") + + except Exception as e: + self.logger.error(f"āŒ Error resuming trading: {e}") + + def get_stop_loss_price(self, entry_price: float, side: str) -> float: + """ + Calculate stop loss price + + Args: + entry_price: Position entry price + side: Position side ('long' or 'short') + + Returns: + Stop loss price + """ + + try: + if side == 'long': + return entry_price * (1 - self.risk_limits.stop_loss_percent / 100) + else: # short + return entry_price * (1 + self.risk_limits.stop_loss_percent / 100) + + except Exception as e: + self.logger.error(f"āŒ Error calculating stop loss price: {e}") + return entry_price + + def get_take_profit_price(self, entry_price: float, side: str) -> float: + """ + Calculate take profit price + + Args: + entry_price: Position entry price + side: Position side ('long' or 'short') + + Returns: + Take profit price + """ + + try: + if side == 'long': + return entry_price * (1 + self.risk_limits.take_profit_percent / 100) + else: # short + return entry_price * (1 - self.risk_limits.take_profit_percent / 100) + + except Exception as e: + self.logger.error(f"āŒ Error calculating take profit price: {e}") + return entry_price + + def calculate_risk_reward_ratio(self, entry_price: float, stop_loss: float, take_profit: float) -> float: + """ + Calculate risk-reward ratio + + Args: + entry_price: Entry price + stop_loss: Stop loss price + take_profit: Take profit price + + Returns: + Risk-reward ratio + """ + + try: + risk = abs(entry_price - stop_loss) + reward = abs(take_profit - entry_price) + + if risk > 0: + return reward / risk + else: + return 0.0 + + except Exception as e: + self.logger.error(f"āŒ Error calculating risk-reward ratio: {e}") + return 0.0 + + def get_risk_metrics(self) -> Dict[str, Any]: + """Get current risk metrics""" + + return { + 'daily_pnl': self.daily_pnl, + 'daily_trades': self.daily_trades, + 'daily_losses': self.daily_losses, + 'max_drawdown': self.max_drawdown, + 'total_exposure': self.total_exposure, + 'trading_halted': self.trading_halted, + 'halt_reason': self.halt_reason, + 'risk_limits': { + 'max_position_size': self.risk_limits.max_position_size, + 'max_daily_loss': self.risk_limits.max_daily_loss, + 'stop_loss_percent': self.risk_limits.stop_loss_percent, + 'take_profit_percent': self.risk_limits.take_profit_percent + } + } + + def is_trading_allowed(self) -> bool: + """Check if trading is allowed""" + + return not self.trading_halted and self.daily_pnl > -self.risk_limits.max_daily_loss + + def get_position_limits(self) -> Dict[str, float]: + """Get position size limits""" + + return { + 'max_position_size': self.risk_limits.max_position_size, + 'current_exposure': self.total_exposure, + 'remaining_capacity': max(0, self.risk_limits.max_portfolio_risk - self.total_exposure) + } \ No newline at end of file diff --git a/trading_bot/bot_core/websocket_manager.py b/trading_bot/bot_core/websocket_manager.py new file mode 100644 index 0000000..1353677 --- /dev/null +++ b/trading_bot/bot_core/websocket_manager.py @@ -0,0 +1,455 @@ +""" +WebSocket Manager for Binance Real-Time Data +Handles WebSocket connections, reconnection logic, and data streaming +""" + +import asyncio +import json +import time +import websockets +from datetime import datetime +from typing import Dict, List, Optional, Callable, Any +from dataclasses import dataclass + +from .config import Config +from .logger import TradingLogger + + +@dataclass +class WebSocketConfig: + """WebSocket configuration""" + url: str + streams: List[str] + reconnect_delay: int = 5 + ping_interval: int = 20 + timeout: int = 30 + + +class WebSocketManager: + """ + Manages WebSocket connections to Binance for real-time data + """ + + def __init__(self, config: Config, logger: TradingLogger, message_handler: Callable[[Dict], None]): + self.config = config + self.logger = logger + self.message_handler = message_handler + + # WebSocket connection + self.websocket: Optional[websockets.WebSocketServerProtocol] = None + self.connection_url: str = "" + self.streams: List[str] = [] + + # Connection state + self.running = False + self.connected = False + self.reconnect_count = 0 + self.last_ping = time.time() + self.last_message = time.time() + + # Statistics + self.messages_received = 0 + self.connection_start = None + self.total_downtime = 0 + + # Setup streams + self._setup_streams() + + # Tasks + self.connection_task: Optional[asyncio.Task] = None + self.ping_task: Optional[asyncio.Task] = None + self.heartbeat_task: Optional[asyncio.Task] = None + + def _setup_streams(self): + """Setup WebSocket streams based on configuration""" + + symbol = self.config.SYMBOL.lower() + + # Price streams + self.streams.extend([ + f"{symbol}@ticker", # 24hr ticker statistics + f"{symbol}@trade", # Trade stream + f"{symbol}@kline_{self.config.TIMEFRAME}", # Kline/candlestick + f"{symbol}@depth", # Order book depth + ]) + + # Additional streams based on config + if self.config.ENABLE_VOLUME_ANALYSIS: + self.streams.append(f"{symbol}@depth20@100ms") # Faster depth updates + + # Build connection URL + base_url = "wss://stream.binance.com:9443" + if self.config.BINANCE_TESTNET: + base_url = "wss://testnet.binance.vision" + + stream_names = "/".join(self.streams) + self.connection_url = f"{base_url}/stream?streams={stream_names}" + + self.logger.info(f"šŸ“” WebSocket streams configured: {len(self.streams)} streams") + self.logger.debug(f"šŸ”— Connection URL: {self.connection_url}") + + async def start(self): + """Start WebSocket connection""" + if self.running: + self.logger.warning("āš ļø WebSocket already running") + return + + self.running = True + self.logger.info("šŸš€ Starting WebSocket connection...") + + # Start connection task + self.connection_task = asyncio.create_task(self._connection_loop()) + + # Start heartbeat monitoring + self.heartbeat_task = asyncio.create_task(self._heartbeat_loop()) + + # Wait for initial connection + await self._wait_for_connection() + + self.logger.info("āœ… WebSocket connection established") + + async def stop(self): + """Stop WebSocket connection""" + if not self.running: + return + + self.logger.info("šŸ›‘ Stopping WebSocket connection...") + self.running = False + + # Cancel tasks + if self.connection_task: + self.connection_task.cancel() + if self.ping_task: + self.ping_task.cancel() + if self.heartbeat_task: + self.heartbeat_task.cancel() + + # Close WebSocket + if self.websocket: + await self.websocket.close() + self.websocket = None + + self.connected = False + self.logger.info("āœ… WebSocket connection stopped") + + async def _connection_loop(self): + """Main connection loop with reconnection logic""" + + while self.running: + try: + await self._connect() + await self._message_loop() + + except websockets.exceptions.ConnectionClosed: + self.logger.warning("šŸ”Œ WebSocket connection closed") + self.connected = False + await self._handle_reconnection() + + except Exception as e: + self.logger.error(f"āŒ WebSocket error: {e}") + self.connected = False + await self._handle_reconnection() + + async def _connect(self): + """Establish WebSocket connection""" + + try: + self.logger.info("šŸ”Œ Connecting to Binance WebSocket...") + + # Create connection + self.websocket = await websockets.connect( + self.connection_url, + ping_interval=self.config.WS_PING_INTERVAL, + ping_timeout=self.config.WS_TIMEOUT, + close_timeout=10 + ) + + self.connected = True + self.connection_start = time.time() + self.last_message = time.time() + + self.logger.websocket("CONNECTED", {"url": self.connection_url}) + + # Start ping task + self.ping_task = asyncio.create_task(self._ping_loop()) + + except Exception as e: + self.logger.error(f"āŒ Connection failed: {e}") + raise + + async def _message_loop(self): + """Main message processing loop""" + + while self.running and self.connected: + try: + # Receive message with timeout + message = await asyncio.wait_for( + self.websocket.recv(), + timeout=self.config.WS_TIMEOUT + ) + + # Process message + await self._process_message(message) + + # Update statistics + self.messages_received += 1 + self.last_message = time.time() + + except asyncio.TimeoutError: + self.logger.warning("ā° WebSocket message timeout") + break + + except websockets.exceptions.ConnectionClosed: + self.logger.warning("šŸ”Œ WebSocket connection closed during message loop") + break + + except Exception as e: + self.logger.error(f"āŒ Error in message loop: {e}") + break + + async def _process_message(self, raw_message: str): + """Process incoming WebSocket message""" + + try: + # Parse JSON + data = json.loads(raw_message) + + # Handle different message types + if 'stream' in data and 'data' in data: + stream_name = data['stream'] + stream_data = data['data'] + + # Process based on stream type + if '@ticker' in stream_name: + await self._handle_ticker_data(stream_data) + elif '@trade' in stream_name: + await self._handle_trade_data(stream_data) + elif '@kline' in stream_name: + await self._handle_kline_data(stream_data) + elif '@depth' in stream_name: + await self._handle_depth_data(stream_data) + else: + self.logger.debug(f"šŸ“¦ Unhandled stream: {stream_name}") + + else: + self.logger.debug(f"šŸ“¦ Unknown message format: {data}") + + except json.JSONDecodeError as e: + self.logger.error(f"āŒ JSON decode error: {e}") + self.logger.debug(f"Raw message: {raw_message}") + + except Exception as e: + self.logger.error(f"āŒ Error processing message: {e}") + + async def _handle_ticker_data(self, data: Dict[str, Any]): + """Handle 24hr ticker data""" + + try: + processed_data = { + 'type': 'ticker', + 'symbol': data['s'], + 'price': float(data['c']), + 'volume': float(data['v']), + 'price_change': float(data['P']), + 'high': float(data['h']), + 'low': float(data['l']), + 'timestamp': int(data['E']) / 1000, # Convert to seconds + } + + # Send to message handler + await self.message_handler(processed_data) + + self.logger.debug(f"šŸ“Š Ticker: {processed_data['symbol']} @ {processed_data['price']}") + + except Exception as e: + self.logger.error(f"āŒ Error handling ticker data: {e}") + + async def _handle_trade_data(self, data: Dict[str, Any]): + """Handle individual trade data""" + + try: + processed_data = { + 'type': 'trade', + 'symbol': data['s'], + 'price': float(data['p']), + 'quantity': float(data['q']), + 'trade_time': int(data['T']) / 1000, + 'is_buyer_maker': data['m'], + 'timestamp': int(data['E']) / 1000, + } + + # Send to message handler + await self.message_handler(processed_data) + + self.logger.debug(f"šŸ’° Trade: {processed_data['quantity']} {processed_data['symbol']} @ {processed_data['price']}") + + except Exception as e: + self.logger.error(f"āŒ Error handling trade data: {e}") + + async def _handle_kline_data(self, data: Dict[str, Any]): + """Handle candlestick/kline data""" + + try: + kline = data['k'] + + # Only process closed candles + if not kline['x']: # x = is_closed + return + + processed_data = { + 'type': 'kline', + 'symbol': kline['s'], + 'open': float(kline['o']), + 'high': float(kline['h']), + 'low': float(kline['l']), + 'close': float(kline['c']), + 'volume': float(kline['v']), + 'trades': int(kline['n']), + 'interval': kline['i'], + 'open_time': int(kline['t']) / 1000, + 'close_time': int(kline['T']) / 1000, + 'timestamp': int(data['E']) / 1000, + } + + # Send to message handler + await self.message_handler(processed_data) + + self.logger.debug(f"šŸ“ˆ Kline: {processed_data['symbol']} OHLC: {processed_data['open']:.2f}/{processed_data['high']:.2f}/{processed_data['low']:.2f}/{processed_data['close']:.2f}") + + except Exception as e: + self.logger.error(f"āŒ Error handling kline data: {e}") + + async def _handle_depth_data(self, data: Dict[str, Any]): + """Handle order book depth data""" + + try: + processed_data = { + 'type': 'depth', + 'symbol': data['s'], + 'bids': [[float(bid[0]), float(bid[1])] for bid in data['b']], + 'asks': [[float(ask[0]), float(ask[1])] for ask in data['a']], + 'timestamp': int(data['E']) / 1000, + } + + # Send to message handler + await self.message_handler(processed_data) + + if processed_data['bids'] and processed_data['asks']: + best_bid = processed_data['bids'][0][0] + best_ask = processed_data['asks'][0][0] + spread = best_ask - best_bid + + self.logger.debug(f"šŸ“Š Depth: {processed_data['symbol']} Bid: {best_bid}, Ask: {best_ask}, Spread: {spread:.4f}") + + except Exception as e: + self.logger.error(f"āŒ Error handling depth data: {e}") + + async def _ping_loop(self): + """Send periodic ping to keep connection alive""" + + while self.running and self.connected: + try: + await asyncio.sleep(self.config.WS_PING_INTERVAL) + + if self.websocket: + await self.websocket.ping() + self.last_ping = time.time() + self.logger.debug("šŸ“ Ping sent") + + except Exception as e: + self.logger.warning(f"āš ļø Ping failed: {e}") + break + + async def _heartbeat_loop(self): + """Monitor connection health""" + + while self.running: + try: + current_time = time.time() + + # Check for stale connection + if self.connected and current_time - self.last_message > 60: # 1 minute + self.logger.warning("šŸ’” Connection appears stale - no messages received") + await self.reconnect() + + # Check ping timeout + if self.connected and current_time - self.last_ping > self.config.WS_PING_INTERVAL * 2: + self.logger.warning("šŸ’” Ping timeout - connection may be dead") + await self.reconnect() + + await asyncio.sleep(30) # Check every 30 seconds + + except Exception as e: + self.logger.error(f"āŒ Error in heartbeat loop: {e}") + await asyncio.sleep(10) + + async def _handle_reconnection(self): + """Handle reconnection with exponential backoff""" + + if not self.running: + return + + self.reconnect_count += 1 + delay = min(self.config.WS_RECONNECT_DELAY * (2 ** (self.reconnect_count - 1)), 60) + + self.logger.info(f"šŸ”„ Reconnecting in {delay}s (attempt #{self.reconnect_count})") + await asyncio.sleep(delay) + + if self.running: + self.logger.info("šŸ”„ Attempting to reconnect...") + + async def reconnect(self): + """Force reconnection""" + + self.logger.info("šŸ”„ Forcing WebSocket reconnection...") + + # Close current connection + if self.websocket: + await self.websocket.close() + self.websocket = None + + self.connected = False + + # Cancel ping task + if self.ping_task: + self.ping_task.cancel() + self.ping_task = None + + async def _wait_for_connection(self, timeout: int = 30): + """Wait for WebSocket connection to be established""" + + start_time = time.time() + + while time.time() - start_time < timeout: + if self.connected: + return + await asyncio.sleep(0.1) + + raise TimeoutError("WebSocket connection timeout") + + def get_statistics(self) -> Dict[str, Any]: + """Get connection statistics""" + + uptime = time.time() - self.connection_start if self.connection_start else 0 + + return { + 'connected': self.connected, + 'running': self.running, + 'messages_received': self.messages_received, + 'reconnect_count': self.reconnect_count, + 'uptime': uptime, + 'streams': len(self.streams), + 'last_message_age': time.time() - self.last_message if self.last_message else 0 + } + + def is_healthy(self) -> bool: + """Check if connection is healthy""" + + if not self.connected: + return False + + # Check if we're receiving messages + if time.time() - self.last_message > 60: # No messages for 1 minute + return False + + return True \ No newline at end of file diff --git a/trading_bot/docker-compose.yml b/trading_bot/docker-compose.yml new file mode 100644 index 0000000..9f108af --- /dev/null +++ b/trading_bot/docker-compose.yml @@ -0,0 +1,224 @@ +version: '3.8' + +services: + trading-bot: + build: + context: . + dockerfile: Dockerfile + target: production + + container_name: binance-trading-bot + + restart: unless-stopped + + environment: + # Binance API Configuration + BINANCE_API_KEY: ${BINANCE_API_KEY:-} + BINANCE_SECRET_KEY: ${BINANCE_SECRET_KEY:-} + BINANCE_TESTNET: ${BINANCE_TESTNET:-true} + + # Trading Configuration + TRADING_SYMBOL: ${TRADING_SYMBOL:-BTCUSDT} + POSITION_SIZE: ${POSITION_SIZE:-0.001} + LEVERAGE: ${LEVERAGE:-1} + MAX_POSITIONS: ${MAX_POSITIONS:-1} + + # Risk Management + STOP_LOSS_PERCENT: ${STOP_LOSS_PERCENT:-2.0} + TAKE_PROFIT_PERCENT: ${TAKE_PROFIT_PERCENT:-3.0} + MAX_DAILY_LOSS: ${MAX_DAILY_LOSS:-5.0} + MAX_PORTFOLIO_RISK: ${MAX_PORTFOLIO_RISK:-10.0} + + # Technical Indicators + BOLLINGER_PERIOD: ${BOLLINGER_PERIOD:-20} + BOLLINGER_STD: ${BOLLINGER_STD:-2.0} + MA_PERIOD: ${MA_PERIOD:-50} + MA_TYPE: ${MA_TYPE:-SMA} + + # Trading Strategy + STRATEGY: ${STRATEGY:-BOLLINGER_MA} + TIMEFRAME: ${TIMEFRAME:-1m} + LOOKBACK_PERIODS: ${LOOKBACK_PERIODS:-100} + + # WebSocket Configuration + WS_RECONNECT_DELAY: ${WS_RECONNECT_DELAY:-5} + WS_PING_INTERVAL: ${WS_PING_INTERVAL:-20} + WS_TIMEOUT: ${WS_TIMEOUT:-30} + + # Logging Configuration + LOG_LEVEL: ${LOG_LEVEL:-INFO} + LOG_FILE: ${LOG_FILE:-logs/trading_bot.log} + LOG_MAX_SIZE: ${LOG_MAX_SIZE:-10485760} + LOG_BACKUP_COUNT: ${LOG_BACKUP_COUNT:-5} + + # Database Configuration + DATABASE_URL: ${DATABASE_URL:-sqlite:///data/trading_bot.db} + TRACK_PERFORMANCE: ${TRACK_PERFORMANCE:-true} + + # Notification Configuration + TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN:-} + TELEGRAM_CHAT_ID: ${TELEGRAM_CHAT_ID:-} + ENABLE_NOTIFICATIONS: ${ENABLE_NOTIFICATIONS:-false} + + # Safety Features + PAPER_TRADING: ${PAPER_TRADING:-true} + ENABLE_TRADING: ${ENABLE_TRADING:-false} + TRADING_HOURS_START: ${TRADING_HOURS_START:-00:00} + TRADING_HOURS_END: ${TRADING_HOURS_END:-23:59} + + # Performance Monitoring + PERFORMANCE_UPDATE_INTERVAL: ${PERFORMANCE_UPDATE_INTERVAL:-60} + SAVE_TRADES_TO_FILE: ${SAVE_TRADES_TO_FILE:-true} + TRADES_FILE_PATH: ${TRADES_FILE_PATH:-data/trades.json} + + # Advanced Features + ENABLE_VOLUME_ANALYSIS: ${ENABLE_VOLUME_ANALYSIS:-false} + VOLUME_THRESHOLD: ${VOLUME_THRESHOLD:-1.5} + ENABLE_SENTIMENT_ANALYSIS: ${ENABLE_SENTIMENT_ANALYSIS:-false} + + # Timezone + TZ: ${TZ:-UTC} + + volumes: + # Persistent data storage + - ./data:/app/data + - ./logs:/app/logs + + # Configuration files (optional) + - ./.env:/app/.env:ro + + # Backup location (optional) + - ./backups:/app/backups + + ports: + # Monitoring port (optional) + - "${MONITORING_PORT:-8000}:8000" + + networks: + - trading-network + + # Resource limits + deploy: + resources: + limits: + cpus: '1.0' + memory: 512M + reservations: + cpus: '0.5' + memory: 256M + + # Health check + healthcheck: + test: ["CMD", "python", "-c", "import os; exit(0 if os.path.exists('/app/logs/trading_bot.log') else 1)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + + # Logging configuration + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + # Optional: Database service for advanced analytics + database: + image: postgres:15-alpine + container_name: trading-bot-db + restart: unless-stopped + + environment: + POSTGRES_DB: ${POSTGRES_DB:-trading_bot} + POSTGRES_USER: ${POSTGRES_USER:-trading_user} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-trading_password} + POSTGRES_HOST_AUTH_METHOD: ${POSTGRES_HOST_AUTH_METHOD:-scram-sha-256} + + volumes: + - postgres_data:/var/lib/postgresql/data + - ./sql:/docker-entrypoint-initdb.d:ro + + ports: + - "${POSTGRES_PORT:-5432}:5432" + + networks: + - trading-network + + # Resource limits + deploy: + resources: + limits: + cpus: '0.5' + memory: 256M + reservations: + cpus: '0.25' + memory: 128M + + # Health check + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-trading_user} -d ${POSTGRES_DB:-trading_bot}"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 30s + + # Optional: Monitoring service + monitoring: + image: prom/prometheus:latest + container_name: trading-bot-monitoring + restart: unless-stopped + + ports: + - "${PROMETHEUS_PORT:-9090}:9090" + + volumes: + - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro + - prometheus_data:/prometheus + + networks: + - trading-network + + depends_on: + - trading-bot + + profiles: + - monitoring + + # Optional: Grafana for visualization + grafana: + image: grafana/grafana:latest + container_name: trading-bot-grafana + restart: unless-stopped + + environment: + - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD:-admin} + + ports: + - "${GRAFANA_PORT:-3000}:3000" + + volumes: + - grafana_data:/var/lib/grafana + - ./monitoring/grafana/dashboards:/etc/grafana/provisioning/dashboards:ro + - ./monitoring/grafana/datasources:/etc/grafana/provisioning/datasources:ro + + networks: + - trading-network + + depends_on: + - monitoring + + profiles: + - monitoring + +networks: + trading-network: + driver: bridge + name: trading-network + +volumes: + postgres_data: + driver: local + prometheus_data: + driver: local + grafana_data: + driver: local \ No newline at end of file diff --git a/trading_bot/env_example b/trading_bot/env_example new file mode 100644 index 0000000..f26e7ac --- /dev/null +++ b/trading_bot/env_example @@ -0,0 +1,183 @@ +# Binance Trading Bot Configuration +# Copy this file to .env and update with your actual values + +# ================================ +# BINANCE API CONFIGURATION +# ================================ +# Get your API keys from: https://www.binance.com/en/my/settings/api-management +BINANCE_API_KEY=your_binance_api_key_here +BINANCE_SECRET_KEY=your_binance_secret_key_here + +# Use testnet for safe testing (highly recommended for beginners) +BINANCE_TESTNET=true + +# ================================ +# TRADING CONFIGURATION +# ================================ +# Trading symbol (e.g., BTCUSDT, ETHUSDT, ADAUSDT) +TRADING_SYMBOL=BTCUSDT + +# Position size in base currency (e.g., 0.001 BTC for BTCUSDT) +POSITION_SIZE=0.001 + +# Leverage (1 = spot trading, >1 = futures trading) +LEVERAGE=1 + +# Maximum concurrent positions +MAX_POSITIONS=1 + +# ================================ +# RISK MANAGEMENT +# ================================ +# Stop loss percentage (e.g., 2.0 = 2% loss) +STOP_LOSS_PERCENT=2.0 + +# Take profit percentage (e.g., 3.0 = 3% profit) +TAKE_PROFIT_PERCENT=3.0 + +# Maximum daily loss percentage +MAX_DAILY_LOSS=5.0 + +# Maximum portfolio risk percentage +MAX_PORTFOLIO_RISK=10.0 + +# ================================ +# TECHNICAL INDICATORS +# ================================ +# Bollinger Bands period (typically 20) +BOLLINGER_PERIOD=20 + +# Bollinger Bands standard deviation (typically 2.0) +BOLLINGER_STD=2.0 + +# Moving Average period (typically 50) +MA_PERIOD=50 + +# Moving Average type (SMA or EMA) +MA_TYPE=SMA + +# ================================ +# TRADING STRATEGY +# ================================ +# Strategy type: BOLLINGER_MA, BOLLINGER_ONLY, MA_CROSSOVER, MEAN_REVERSION +STRATEGY=BOLLINGER_MA + +# Timeframe for analysis (1m, 3m, 5m, 15m, 30m, 1h, 4h, 1d) +TIMEFRAME=1m + +# Number of historical periods to analyze +LOOKBACK_PERIODS=100 + +# ================================ +# WEBSOCKET CONFIGURATION +# ================================ +# WebSocket reconnection delay in seconds +WS_RECONNECT_DELAY=5 + +# WebSocket ping interval in seconds +WS_PING_INTERVAL=20 + +# WebSocket timeout in seconds +WS_TIMEOUT=30 + +# ================================ +# LOGGING CONFIGURATION +# ================================ +# Log level: DEBUG, INFO, WARNING, ERROR, CRITICAL +LOG_LEVEL=INFO + +# Log file path +LOG_FILE=logs/trading_bot.log + +# Maximum log file size in bytes (10MB) +LOG_MAX_SIZE=10485760 + +# Number of backup log files to keep +LOG_BACKUP_COUNT=5 + +# ================================ +# DATABASE CONFIGURATION +# ================================ +# Database URL for performance tracking +DATABASE_URL=sqlite:///data/trading_bot.db + +# Enable performance tracking +TRACK_PERFORMANCE=true + +# ================================ +# NOTIFICATION CONFIGURATION +# ================================ +# Telegram bot token for notifications (optional) +TELEGRAM_BOT_TOKEN= + +# Telegram chat ID for notifications (optional) +TELEGRAM_CHAT_ID= + +# Enable notifications +ENABLE_NOTIFICATIONS=false + +# ================================ +# SAFETY FEATURES +# ================================ +# Paper trading mode (highly recommended for testing) +PAPER_TRADING=true + +# Enable actual trading (set to false for safety) +ENABLE_TRADING=false + +# Trading hours (24-hour format, e.g., 09:00-17:00) +TRADING_HOURS_START=00:00 +TRADING_HOURS_END=23:59 + +# ================================ +# PERFORMANCE MONITORING +# ================================ +# Performance update interval in seconds +PERFORMANCE_UPDATE_INTERVAL=60 + +# Save trades to file +SAVE_TRADES_TO_FILE=true + +# Trades file path +TRADES_FILE_PATH=data/trades.json + +# ================================ +# ADVANCED FEATURES +# ================================ +# Enable volume analysis +ENABLE_VOLUME_ANALYSIS=false + +# Volume threshold for analysis +VOLUME_THRESHOLD=1.5 + +# Enable sentiment analysis (experimental) +ENABLE_SENTIMENT_ANALYSIS=false + +# ================================ +# DOCKER CONFIGURATION +# ================================ +# Monitoring port for health checks +MONITORING_PORT=8000 + +# Timezone for the container +TZ=UTC + +# ================================ +# DATABASE CONFIGURATION (Optional) +# ================================ +# PostgreSQL configuration for advanced analytics +POSTGRES_DB=trading_bot +POSTGRES_USER=trading_user +POSTGRES_PASSWORD=secure_password_here +POSTGRES_PORT=5432 +POSTGRES_HOST_AUTH_METHOD=scram-sha-256 + +# ================================ +# MONITORING CONFIGURATION (Optional) +# ================================ +# Prometheus port +PROMETHEUS_PORT=9090 + +# Grafana port and password +GRAFANA_PORT=3000 +GRAFANA_PASSWORD=admin_password_here \ No newline at end of file diff --git a/trading_bot/main.py b/trading_bot/main.py new file mode 100644 index 0000000..01c3329 --- /dev/null +++ b/trading_bot/main.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +""" +Binance Trading Bot - Main Entry Point +Advanced crypto trading bot with WebSocket connection, technical indicators, +and automated trading capabilities. + +Features: +- Real-time Binance WebSocket connection +- Bollinger Bands & Moving Average indicators +- Automated buy/sell execution +- Risk management with stop-loss/take-profit +- Performance tracking system +- Docker deployment ready +- Environment variable configuration +""" + +import asyncio +import logging +import signal +import sys +from datetime import datetime +from typing import Dict, Any + +from bot_core.bot import TradingBot +from bot_core.config import Config +from bot_core.logger import setup_logger + + +class BotManager: + """Main bot manager that handles lifecycle and signals""" + + def __init__(self): + self.bot: TradingBot = None + self.config: Config = None + self.logger = None + self.running = False + + async def initialize(self): + """Initialize bot configuration and components""" + try: + # Load configuration + self.config = Config() + + # Setup logging + self.logger = setup_logger( + name="TradingBot", + level=self.config.LOG_LEVEL, + log_file=self.config.LOG_FILE + ) + + self.logger.info("=" * 60) + self.logger.info("šŸš€ BINANCE TRADING BOT STARTING") + self.logger.info("=" * 60) + + # Log configuration + self.logger.info(f"Trading Symbol: {self.config.SYMBOL}") + self.logger.info(f"Position Size: {self.config.POSITION_SIZE}") + self.logger.info(f"Risk Management: SL={self.config.STOP_LOSS_PERCENT}%, TP={self.config.TAKE_PROFIT_PERCENT}%") + self.logger.info(f"Bollinger Bands: Period={self.config.BOLLINGER_PERIOD}, STD={self.config.BOLLINGER_STD}") + self.logger.info(f"Moving Average: Period={self.config.MA_PERIOD}") + + # Initialize trading bot + self.bot = TradingBot(self.config, self.logger) + await self.bot.initialize() + + self.logger.info("āœ… Bot initialization completed successfully") + + except Exception as e: + if self.logger: + self.logger.error(f"āŒ Failed to initialize bot: {e}") + else: + print(f"āŒ Failed to initialize bot: {e}") + raise + + async def start(self): + """Start the trading bot""" + try: + self.running = True + self.logger.info("šŸŽÆ Starting trading bot...") + + # Start bot + await self.bot.start() + + # Keep running until stopped + while self.running: + await asyncio.sleep(1) + + except Exception as e: + self.logger.error(f"āŒ Bot encountered error: {e}") + await self.stop() + raise + + async def stop(self): + """Stop the trading bot gracefully""" + if not self.running: + return + + self.logger.info("šŸ›‘ Stopping trading bot...") + self.running = False + + if self.bot: + await self.bot.stop() + + self.logger.info("āœ… Bot stopped successfully") + + def setup_signal_handlers(self): + """Setup signal handlers for graceful shutdown""" + def signal_handler(signum, frame): + self.logger.info(f"šŸ“” Received signal {signum}, initiating graceful shutdown...") + asyncio.create_task(self.stop()) + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + +async def main(): + """Main function""" + bot_manager = BotManager() + + try: + # Setup signal handlers + bot_manager.setup_signal_handlers() + + # Initialize and start bot + await bot_manager.initialize() + await bot_manager.start() + + except KeyboardInterrupt: + print("\nšŸ”“ KeyboardInterrupt received") + await bot_manager.stop() + + except Exception as e: + print(f"āŒ Critical error: {e}") + await bot_manager.stop() + sys.exit(1) + + +if __name__ == "__main__": + try: + asyncio.run(main()) + except KeyboardInterrupt: + print("\nšŸ‘‹ Bot shutdown complete") + except Exception as e: + print(f"āŒ Fatal error: {e}") + sys.exit(1) \ No newline at end of file diff --git a/trading_bot/requirements.txt b/trading_bot/requirements.txt new file mode 100644 index 0000000..7105101 --- /dev/null +++ b/trading_bot/requirements.txt @@ -0,0 +1,70 @@ +# Trading Bot Requirements +# Core dependencies for automated crypto trading bot + +# Exchange connectivity +ccxt>=4.0.0 +websockets>=11.0.0 + +# Data processing and analysis +pandas>=2.0.0 +numpy>=1.24.0 +pandas-ta>=0.3.14b + +# Async support +aiohttp>=3.8.0 +asyncio-throttle>=1.0.0 + +# Configuration management +python-dotenv>=1.0.0 + +# Database support +sqlalchemy>=2.0.0 +aiosqlite>=0.19.0 + +# Logging and monitoring +colorlog>=6.7.0 + +# Mathematical computations +scipy>=1.10.0 +scikit-learn>=1.3.0 + +# Time and date handling +pytz>=2023.3 + +# Data validation +pydantic>=2.0.0 + +# HTTP requests +requests>=2.31.0 + +# JSON handling +ujson>=5.8.0 + +# Testing (optional) +pytest>=7.4.0 +pytest-asyncio>=0.21.0 + +# Performance monitoring +psutil>=5.9.0 + +# Notifications (optional) +python-telegram-bot>=20.0.0 + +# Cryptographic functions +cryptography>=41.0.0 + +# Environment and system +python-environ>=0.4.0 + +# Development tools (optional) +black>=23.0.0 +flake8>=6.0.0 +mypy>=1.5.0 + +# Jupyter notebook support (optional) +jupyter>=1.0.0 +ipython>=8.0.0 + +# Additional utilities +tqdm>=4.65.0 +rich>=13.0.0 \ No newline at end of file