diff --git a/CHANGELOG.md b/CHANGELOG.md index b4b5d73..4d7dbec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,31 @@ --- +## ๐Ÿ“ˆ **v2.1.0 - "Agnostic Revolution"** โœจ (December 2025) + +### ๐ŸŒ **Broker Agnostic Architecture** โญโญโญโญโญ + +- **Universal Broker Interface**: Complete abstraction of broker logic via `BrokerInterface`. +- **Multi-Platform Support**: Now supports Crypto Exchanges (Binance, Bybit) via CCXT integration. +- **Docker & Cloud Ready**: Crypto bots can now run on Linux/Docker without needing a local MT5 terminal. +- **Broker Factory**: Dynamic switching between MT5, CCXT, and Mock adapters. +- **Stateful Mock Testing**: Advanced simulation mode with `visual_simulation.py` for risk-free testing. + +### โšก **System & Performance Optimization** + +- **Python 3.13 Support**: Optimized for the latest stable Python versions (3.10 - 3.13). +- **Dependency Refresh**: Fixed "CPU 100%" issue during installation by optimizing `requirements.txt`. +- **Binary Wheels**: Configured dependencies to use pre-compiled binaries for faster setup. +- **Agnostic Symbol Mapping**: Automatic translation between MT5 (EURUSD) and CCXT (BTC/USDT) formats. + +### ๐Ÿงช **New Testing Framework** + +- **Agnostic Bot Validator**: `test_agnostic_bot.py` for cross-platform logic verification. +- **CCXT Connection Suite**: Dedicated tools for validating exchange connectivity and demo trading. +- **Visual Simulation Engine**: Real-time market mock-up for strategy debugging. + +--- + ## ๐ŸŽฏ **Investment Highlights** **Why QuantumBotX Stands Out:** diff --git a/QUICK_START_GUIDE.md b/QUICK_START_GUIDE.md index d494e94..f6693dc 100644 --- a/QUICK_START_GUIDE.md +++ b/QUICK_START_GUIDE.md @@ -1,12 +1,16 @@ # QuantumBotX - Quick Start Guide -## First Time Setup +## Setup Your Broker -1. **Install MetaTrader 5** (Required) - - Download from: - - Install and create a demo account - - Keep MT5 running in the background - - โš ๏ธ **IMPORTANT:** MetaTrader 5 must be running for QuantumBotX to work +- **Option A: MetaTrader 5 (Forex/Gold)** + - Download MT5 from: + - Install and keep it running in the background. + +- **Option B: Crypto Exchange (Binance/Bybit)** + - Create an account on your preferred exchange. + - For testing, use **Binance Futures Testnet**. + +โš ๏ธ **Note:** MT5 is only required if you choose to trade Forex/Gold via `MT5` broker type. 2. **Configure Your Settings** - Copy `.env.example` to `.env` @@ -18,18 +22,20 @@ MT5_SERVER=your_server_name ``` -3. **Start the Application** +3. **Verify Connection** + - To test MT5: `python test_mt5_connection.py` + - To test Crypto: `python test_ccxt.py` + - To simulate without Internet: `python visual_simulation.py` + +4. **Start the Application** - Double-click `start.bat` (Windows) - - Open in your browser + - Open -## โœ… System Requirements - -- **Windows 7 SP1 or later** (64-bit recommended) -- **MetaTrader 5** (must be installed separately) -- **4GB RAM minimum** (8GB recommended) -- **500MB free disk space** -- **Internet connection** for initial setup -- **โŒ Python NOT required** (already bundled in the installer) +- **Windows 10/11** (Native MT5 support) +- **Linux/MacOS/Docker** (CCXT/Crypto support only) +- **Python 3.10 - 3.13** +- **4GB RAM minimum** +- **Stable internet connection** ## Daily Use diff --git a/README.md b/README.md index 965b40c..477057c 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,20 @@ -# ๐Ÿค– QuantumBotX โ€” AI-Powered Modular Trading Bot for MT5 +# ๐Ÿค– QuantumBotX โ€” AI-Powered Broker Agnostic Trading Bot -!MIT License -!Python Version -!Framework -!Made with Love - -Welcome to **QuantumBotX**, your personal, modular, and smart trading assistant built with Python and MetaTrader5 (MT5). -Designed to be elegant, powerful, and flexible โ€” whether you're a scalper, swing trader, or a strategy researcher. +Welcome to **QuantumBotX**, your personal, modular, and smart trading assistant. Now powered by a **Broker Agnostic Architecture**, allowing you to trade across MetaTrader5 (MT5) and major Crypto Exchanges (Binance, Bybit, etc.) using a single unified interface. --- -## โš ๏ธ Platform Support Notice +## ๐ŸŒ Platform & Broker Support -### **Primary Platform: Windows** ๐ŸชŸ +### **Flexible Deployment** ๐Ÿš€ +- **Windows**: Native support for MT5 and CCXT. +- **Linux/Docker/Cloud**: Full support for Crypto Trading via CCXT (no MT5 required!). +- **Local MT5**: Requires Windows (or Wine) for Forex/Gold trading. -This version of QuantumBotX is **optimized for Windows** and requires MetaTrader 5 terminal to be installed locally. It's designed for learning algorithmic trading on your personal computer. - -### **Alternative Platforms** ๐Ÿ”„ - -- **Linux**: Can attempt using Wine (experimental - see Linux Setup guide below) -- **macOS**: Not officially supported (requires Wine or Windows VM) -- **Cloud/VPS**: Not compatible (requires local MT5 terminal) - -> ๐Ÿ’ก **Pro Tip**: For cloud deployment and multi-platform support, check out our upcoming **QuantumBotX API** version! +### **Supported Brokers** ๐Ÿฆ +- **MetaTrader 5**: XM, Exness, FBS, IC Markets, etc. +- **Crypto Exchanges (via CCXT)**: Binance (Spot/Futures), Bybit, OKX, and 100+ others. +- **Simulation**: Built-in **Mock Broker** for risk-free strategy testing without internet. --- @@ -90,38 +82,28 @@ This version of QuantumBotX is **optimized for Windows** and requires MetaTrader --- -## ๐Ÿš€ Development & Testing Framework +### ๐Ÿงช **Testing & Simulation Infrastructure** -### ๐Ÿงช **Testing Infrastructure** - -- โœ… **30+ Test Scripts**: Comprehensive testing suite in dedicated `testing/` directory -- โœ… **Multi-Broker Testing**: XM Global, Exness, Alpari compatibility validation -- โœ… **Strategy Validation**: Individual strategy testing and parameter optimization -- โœ… **ATR Education Testing**: Interactive examples and beginner tutorials -- โœ… **Crypto Integration Tests**: Bitcoin/Ethereum weekend mode validation -- โœ… **Indonesian Market Tests**: XM Indonesia and IDR pairs testing -- โœ… **Risk Management Tests**: XAUUSD protection and ATR-based sizing validation +- โœ… **Agnostic Testing**: `test_agnostic_bot.py` validates logic across different broker types. +- โœ… **Visual Simulation**: `visual_simulation.py` provides a real-time "Mock Market" for strategy debugging. +- โœ… **CCXT Validation**: dedicated `test_ccxt.py` and `test_ccxt_order.py` for exchange connectivity. +- โœ… **30+ Test Scripts**: Comprehensive testing suite in dedicated `testing/` directory. ### ๐Ÿ”ง **Development Tools** - -- โœ… **Symbol Migration Tools**: Automatic broker symbol discovery and mapping -- โœ… **Bot State Management**: Debug and fix tools for bot recovery -- โœ… **Performance Analysis**: Backtesting debugging and optimization tools -- โœ… **Market Diagnostics**: Real-time market condition analysis -- โœ… **Integration Demos**: Complete workflow demonstrations +- โœ… **Broker Factory**: Dynamic adapter switching between MT5, CCXT, and Mock providers. +- โœ… **Symbol Discovery**: Automatic mapping between Forex (EURUSD) and Crypto (BTC/USDT) formats. > **Note**: All testing scripts are excluded from git repository for clean production deployment --- -## ๐Ÿ“ฆ Tech Stack - -- `Python 3.10+` -- `Flask` & `TailwindCSS` +- `Python 3.10 - 3.13` (Recommended: 3.13 for best library compatibility) +- `CCXT` (Crypto Exchange Hybrid Integration) +- `Flask` & `Vanilla CSS` (Modern Aesthetic) - `MetaTrader5` Python Integration -- `pandas` & `pandas-ta` for data analysis -- `Chart.js` for data visualization -- `SQLite` for database +- `pandas` & `pandas-ta` (Financial Engineering) +- `Chart.js` (Simulasi & Result Visualization) +- `SQLite` (Local Database) --- @@ -211,14 +193,24 @@ This version of QuantumBotX is **optimized for Windows** and requires MetaTrader --- -## ๐Ÿ” Environment Variables (`.env`) - Rename `.env.example` to `.env`, and fill in the following: ```env +# --- BROKER SELECTION --- +BROKER_TYPE="MT5" # Options: MT5, CCXT, MOCK + +# --- MT5 CONFIG (If MT5 selected) --- MT5_LOGIN="your_mt5_login" MT5_PASSWORD="your_password" MT5_SERVER="your_broker_server" + +# --- CCXT CONFIG (If CCXT selected) --- +EXCHANGE_ID="binance" +CCXT_API_KEY="your_api_key" +CCXT_API_SECRET="your_api_secret" +CCXT_TESTNET=true + +# --- APP CONFIG --- SECRET_KEY="any_flask_secret_key" DB_NAME=bots.db ``` diff --git a/README_NEW.md b/README_NEW.md index 0828ca0..520b663 100644 --- a/README_NEW.md +++ b/README_NEW.md @@ -193,6 +193,7 @@ python run.py ### Current Features โœ… +- โœ… **Agnostic Architecture**: Support for MT5, CCXT (Binance), and Mock Brokers - โœ… MT5 Integration with 50+ instruments - โœ… 16 Trading strategies with risk management - โœ… AI mentor in Indonesian diff --git a/ROADMAP.md b/ROADMAP.md index e0a6412..20d1168 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -4,12 +4,20 @@ ## ๐ŸŽฏ **What's Coming Next** -### **Q4 2025: Intelligence Enhancement** +### **Q4 2025: Agnostic Revolution (RELEASED)** ๐Ÿš€ + +We've officially laid the foundation for the **QuantumBotX API** by implementing a Broker Agnostic Architecture: -- **Advanced AI Features**: Enhanced strategy analysis with machine learning -- **Real-time Notifications**: Telegram integration for trade alerts -- **Portfolio Analytics**: Advanced performance dashboards -- **Enterprise Features**: Multi-account management and compliance logging +- โœ… **Multi-Broker Support**: Dynamic switching between MT5, Binance, and Mock adapters via `BrokerFactory`. +- โœ… **Cloud Ready**: Run on Linux/Docker for Crypto trading without local MT5. +- โœ… **Stateful Simulation**: Real-time mock testing with `visual_simulation.py`. +- โœ… **Modern Foundation**: Optimized for Python 3.13 with seamingless dependency management. + +### **Q1 2026: Intelligence Enhancement (NEXT)** + +- ๐Ÿ”„ **Telegram Notifications**: Real-time trade and error alerts. +- ๐Ÿ”„ **Portfolio Analytics**: Advanced performance dashboards. +- ๐Ÿ”„ **AI Strategy Optimizer**: Automated parameter tuning based on market regime. ### **Exciting New Project** ๐Ÿš€ diff --git a/core/adapters/ccxt_adapter.py b/core/adapters/ccxt_adapter.py index 4f7ba3b..17558f0 100644 --- a/core/adapters/ccxt_adapter.py +++ b/core/adapters/ccxt_adapter.py @@ -29,20 +29,21 @@ class CCXTAdapter(BrokerInterface): 'options': {'defaultType': 'future'} # Default to futures for bots } - # Enable testnet if configured + if credentials.get('PASSWORD'): + config['password'] = credentials.get('PASSWORD') + + # Enable testnet/demo if configured if credentials.get('TESTNET', False): + config['options']['demo'] = True if self.exchange_id == 'binance': + # Manually point to Futures Testnet URL to be extra safe config['urls'] = { 'api': { - 'public': 'https://testnet.binance.vision/api', - 'private': 'https://testnet.binance.vision/api', + 'public': 'https://testnet.binancefuture.com/fapi/v1', + 'private': 'https://testnet.binancefuture.com/fapi/v1', } } - logger.info("Using Binance TESTNET (https://testnet.binance.vision)") - # Add other exchange testnet URLs as needed - - if credentials.get('PASSWORD'): # For exchanges like KuCoin - config['password'] = credentials.get('PASSWORD') + logger.info(f"Using {self.exchange_id} DEMO TRADING mode") self.exchange = exchange_class(config) diff --git a/core/adapters/mt5_adapter.py b/core/adapters/mt5_adapter.py index cd9cd33..9c796d1 100644 --- a/core/adapters/mt5_adapter.py +++ b/core/adapters/mt5_adapter.py @@ -7,68 +7,79 @@ from typing import Dict, Any, List, Optional try: from core.utils.mt5 import ( initialize_mt5, - shutdown_mt5, - get_symbol_info, - get_rates as get_rates_mt5, - place_trade, - close_trade, - get_open_positions, + get_rates_mt5, get_account_info_mt5, get_open_positions_mt5, find_mt5_symbol, TIMEFRAME_MAP, get_todays_profit_mt5 ) + # Corrected import for trade functions + from core.mt5.trade import place_trade as mt5_place_trade, close_trade as mt5_close_trade import MetaTrader5 as mt5 MT5_AVAILABLE = True except ImportError: MT5_AVAILABLE = False - logging.warning("MetaTrader5 module not found. MT5Adapter will not work.") + logger.error("MetaTrader5 module or dependencies not found. MT5Adapter will not work.") logger = logging.getLogger(__name__) class MT5Adapter(BrokerInterface): """ Adapter for MetaTrader 5 using the official python library. - Wraps the functions from core.utils.mt5. + Wraps the functions from core.utils.mt5 and core.mt5.trade. """ def initialize(self, credentials: Dict[str, Any]) -> bool: - # MT5 usually initialized via run.py, but we can support re-init here - return True + """Login to MT5 if provided credentials, else assume already initialized.""" + if not credentials: + return mt5.initialize() if MT5_AVAILABLE else False + + login = credentials.get('MT5_LOGIN') or credentials.get('login') + password = credentials.get('MT5_PASSWORD') or credentials.get('password') + server = credentials.get('MT5_SERVER') or credentials.get('server', 'MetaQuotes-Demo') + + if login and password: + return initialize_mt5(int(login), password, server) + return mt5.initialize() if MT5_AVAILABLE else False def get_account_info(self) -> Optional[Dict[str, Any]]: return get_account_info_mt5() def get_rates(self, symbol: str, timeframe: str, count: int = 100) -> pd.DataFrame: - # Convert string timeframe (e.g. "H1") to MT5 constant mt5_timeframe = TIMEFRAME_MAP.get(timeframe, mt5.TIMEFRAME_H1) - - # Ensure symbol is valid for this broker valid_symbol = find_mt5_symbol(symbol) if not valid_symbol: logger.error(f"Symbol {symbol} not found in MT5") return pd.DataFrame() - return get_rates_mt5(valid_symbol, mt5_timeframe, count) def get_open_positions(self) -> List[Dict[str, Any]]: - return get_open_positions_mt5() + mt5_positions = get_open_positions_mt5() + standardized_positions = [] + for pos in mt5_positions: + # Map MT5 type (0 for Buy, 1 for Sell) to string + standardized_type = 'BUY' if pos.get('type') == mt5.POSITION_TYPE_BUY else 'SELL' + pos['type'] = standardized_type + standardized_positions.append(pos) + return standardized_positions def place_order(self, symbol: str, order_type: str, volume: float, price: float = 0.0, sl: float = 0.0, tp: float = 0.0, comment: str = "") -> bool: valid_symbol = find_mt5_symbol(symbol) if not valid_symbol: return False - # Basic order logic - simplified for adapter POC - action = mt5.TRADE_ACTION_DEAL - type_op = mt5.ORDER_TYPE_BUY if order_type == 'BUY' else mt5.ORDER_TYPE_SELL + mt5_order_type = mt5.ORDER_TYPE_BUY if order_type == 'BUY' else mt5.ORDER_TYPE_SELL + + # We use the existing place_trade logic but wrap the arguments + # Wait, the existing place_trade uses ATR multipliers. + # For the universal adapter, we want raw SL/TP values. request = { - "action": action, + "action": mt5.TRADE_ACTION_DEAL, "symbol": valid_symbol, "volume": volume, - "type": type_op, + "type": mt5_order_type, "price": mt5.symbol_info_tick(valid_symbol).ask if order_type == 'BUY' else mt5.symbol_info_tick(valid_symbol).bid, "sl": sl, "tp": tp, @@ -80,21 +91,29 @@ class MT5Adapter(BrokerInterface): } result = mt5.order_send(request) - if result.retcode != mt5.TRADE_RETCODE_DONE: - logger.error(f"Order failed: {result.comment}") + if result is None or result.retcode != mt5.TRADE_RETCODE_DONE: + logger.error(f"Order failed: {result.comment if result else 'No result'}") return False logger.info(f"Order placed: {result.order}") return True - def close_position(self, position_id: str, volume: float = 0.0) -> bool: + def close_position(self, ticket_id: Any, volume: float = 0.0) -> bool: + """Close an existing position in MT5.""" try: - # Logic to close position... - # For now, returning False as placeholder - pass - except: - pass - return False + # Find the position by ticket + positions = mt5.positions_get(ticket=int(ticket_id)) + if not positions: + logger.warning(f"Position #{ticket_id} not found to close.") + return False + + position = positions[0] + # Use the existing close_trade utility + result, msg = mt5_close_trade(position) + return result is not None + except Exception as e: + logger.error(f"Error closing position {ticket_id}: {e}") + return False def get_symbol_info(self, symbol: str) -> Optional[Dict[str, Any]]: valid_symbol = find_mt5_symbol(symbol) diff --git a/core/bots/trading_bot.py b/core/bots/trading_bot.py index 23476a5..702e74d 100644 --- a/core/bots/trading_bot.py +++ b/core/bots/trading_bot.py @@ -6,8 +6,7 @@ import logging from datetime import datetime from core.strategies.strategy_map import STRATEGY_MAP from core.factory.broker_factory import BrokerFactory -# from core.mt5.trade import place_trade, close_trade <-- DEPRECATED -from core.utils.mt5 import TIMEFRAME_MAP # Keep for now or move to adapter +# from core.mt5.trade import place_trade, close_trade # DEPRECATED # AI Mentor Integration from core.db.models import log_trade_for_ai_analysis # Holiday and market hours management @@ -37,9 +36,6 @@ class TradingBot(threading.Thread): self.last_analysis = {"signal": "MEMUAT", "explanation": "Bot sedang memulai, menunggu analisis pertama..."} self._stop_event = threading.Event() self.strategy_instance = None - self.strategy_instance = None - # Gunakan map yang diimpor untuk menjaga konsistensi - self.tf_map = TIMEFRAME_MAP # Initialize Broker Adapter if broker: @@ -241,7 +237,7 @@ class TradingBot(threading.Thread): # Logika untuk sinyal BUY if signal == 'BUY': # Jika ada posisi SELL, tutup dulu - if position and position.get('type') == 1: # 1 is SELL in MT5, Adapter should standardize this later + if position and position.get('type') == 'SELL': self.log_activity('CLOSE SELL', "Menutup posisi JUAL untuk membuka posisi BELI.", is_notification=True) # Log untuk AI mentor analysis @@ -266,7 +262,7 @@ class TradingBot(threading.Thread): # Logika untuk sinyal SELL elif signal == 'SELL': # Jika ada posisi BUY, tutup dulu - if position and position.get('type') == 0: # 0 is BUY in MT5 + if position and position.get('type') == 'BUY': self.log_activity('CLOSE BUY', "Menutup posisi BELI untuk membuka posisi JUAL.", is_notification=True) # Log untuk AI mentor analysis diff --git a/core/brokers/broker_factory.py b/core/brokers/broker_factory.py index b50d4d3..6870bdf 100644 --- a/core/brokers/broker_factory.py +++ b/core/brokers/broker_factory.py @@ -9,6 +9,7 @@ from typing import Dict, Optional, List from enum import Enum from .base_broker import BaseBroker +from .mt5_broker import MT5Broker from .binance_broker import BinanceBroker from .ctrader_broker import CTraderBroker from .interactive_brokers import InteractiveBrokersBroker @@ -65,7 +66,9 @@ class BrokerFactory: config = broker_config['config'] try: - if broker_type == BrokerType.BINANCE: + if broker_type == BrokerType.MT5: + broker = MT5Broker() + elif broker_type == BrokerType.BINANCE: broker = BinanceBroker(testnet=config.get('testnet', True)) elif broker_type == BrokerType.BINANCE_FUTURES: # Future implementation diff --git a/core/brokers/mt5_broker.py b/core/brokers/mt5_broker.py new file mode 100644 index 0000000..8eb3a38 --- /dev/null +++ b/core/brokers/mt5_broker.py @@ -0,0 +1,198 @@ +# core/brokers/mt5_broker.py +""" +MetaTrader 5 Broker Implementation +Connects QuantumBotX to MT5 terminals via the universal BaseBroker interface. +""" + +import logging +import pandas as pd +import MetaTrader5 as mt5 +from typing import Dict, List, Optional, Union +from datetime import datetime, timedelta + +from .base_broker import BaseBroker, OrderType, OrderStatus, Timeframe, Position, Order, AccountInfo +from core.utils.mt5 import get_rates_mt5, TIMEFRAME_MAP as MT5_TIMEFRAME_MAP + +logger = logging.getLogger(__name__) + +class MT5Broker(BaseBroker): + """ + MT5 Implementation of the BaseBroker. + Wraps MetaTrader5 library calls into a unified API. + """ + + def __init__(self, broker_name: str = "MetaTrader 5"): + super().__init__(broker_name) + self.timeframe_map = { + Timeframe.M1: mt5.TIMEFRAME_M1, + Timeframe.M5: mt5.TIMEFRAME_M5, + Timeframe.M15: mt5.TIMEFRAME_M15, + Timeframe.M30: mt5.TIMEFRAME_M30, + Timeframe.H1: mt5.TIMEFRAME_H1, + Timeframe.H4: mt5.TIMEFRAME_H4, + Timeframe.D1: mt5.TIMEFRAME_D1 + } + + def connect(self, credentials: Dict) -> bool: + """Connect to MT5 terminal""" + try: + login = credentials.get('login') + password = credentials.get('password') + server = credentials.get('server', 'MetaQuotes-Demo') + + if not mt5.initialize(login=int(login), password=password, server=server): + logger.error(f"MT5 initialization failed: {mt5.last_error()}") + self.is_connected = False + return False + + self.is_connected = True + self.supported_symbols = [s.name for s in mt5.symbols_get()] + logger.info("MT5 connected successfully.") + return True + except Exception as e: + logger.error(f"Error connecting to MT5: {e}") + return False + + def disconnect(self) -> bool: + """Disconnect from MT5""" + mt5.shutdown() + self.is_connected = False + return True + + def get_symbols(self) -> List[str]: + """Get list of available trading symbols""" + if not self.is_connected: + return [] + symbols = mt5.symbols_get() + return [s.name for s in symbols] if symbols else [] + + def get_market_data(self, symbol: str, timeframe: Timeframe, + count: int = 500) -> pd.DataFrame: + """Get OHLCV market data from MT5""" + mt5_tf = self.timeframe_map.get(timeframe, mt5.TIMEFRAME_H1) + return get_rates_mt5(symbol, mt5_tf, count) + + def get_current_price(self, symbol: str) -> Dict[str, float]: + """Get current bid/ask prices""" + tick = mt5.symbol_info_tick(symbol) + if tick: + return {"bid": tick.bid, "ask": tick.ask} + return {"bid": 0.0, "ask": 0.0} + + def place_order(self, symbol: str, order_type: OrderType, side: str, + size: float, price: Optional[float] = None, + stop_loss: Optional[float] = None, + take_profit: Optional[float] = None) -> Optional[Order]: + """Place a trading order in MT5""" + if not self.is_connected: + return None + + # Map OrderType to MT5 constant + mt5_type = None + if order_type == OrderType.MARKET_BUY: + mt5_type = mt5.ORDER_TYPE_BUY + elif order_type == OrderType.MARKET_SELL: + mt5_type = mt5.ORDER_TYPE_SELL + # ... support other types as needed + + if mt5_type is None: + logger.error(f"Unsupported order type for MT5: {order_type}") + return None + + curr_price = price or (self.get_current_price(symbol)['ask'] if mt5_type == mt5.ORDER_TYPE_BUY else self.get_current_price(symbol)['bid']) + + request = { + "action": mt5.TRADE_ACTION_DEAL, + "symbol": symbol, + "volume": size, + "type": mt5_type, + "price": curr_price, + "sl": stop_loss or 0.0, + "tp": take_profit or 0.0, + "magic": 2024001, # Default magic number + "comment": "QuantumBotX Trade", + "type_time": mt5.ORDER_TIME_GTC, + "type_filling": mt5.ORDER_FILLING_FOK, + } + + result = mt5.order_send(request) + if result and result.retcode == mt5.TRADE_RETCODE_DONE: + order = Order(str(result.order), symbol, order_type, side, size, curr_price) + order.status = OrderStatus.FILLED + return order + else: + logger.error(f"MT5 Order failed: {result.comment if result else 'Unknown error'}") + return None + + def cancel_order(self, order_id: str) -> bool: + """Cancel an existing order (MT5 usually handles this via close or delete pending)""" + # Simplistic implementation for now + return False + + def get_positions(self) -> List[Position]: + """Get all open positions from MT5""" + mt5_positions = mt5.positions_get() + positions = [] + if mt5_positions: + for p in mt5_positions: + side = 'long' if p.type == mt5.POSITION_TYPE_BUY else 'short' + positions.append(Position( + symbol=p.symbol, + side=side, + size=p.volume, + entry_price=p.price_open, + current_price=p.price_current, + unrealized_pnl=p.profit + )) + return positions + + def get_orders(self) -> List[Order]: + """Get all pending orders from MT5""" + mt5_orders = mt5.orders_get() + orders = [] + if mt5_orders: + for o in mt5_orders: + # Map MT5 order types back to our OrderType + # This is a simplification + order_type = OrderType.LIMIT_BUY if o.type == mt5.ORDER_TYPE_BUY_LIMIT else OrderType.LIMIT_SELL + orders.append(Order( + order_id=str(o.ticket), + symbol=o.symbol, + order_type=order_type, + side='buy' if 'BUY' in order_type.name else 'sell', + size=o.volume_initial, + price=o.price_open + )) + return orders + + def get_account_info(self) -> AccountInfo: + """Get account information from MT5""" + inf = mt5.account_info() + if inf: + return AccountInfo( + balance=inf.balance, + equity=inf.equity, + margin=inf.margin, + free_margin=inf.margin_free, + margin_level=inf.margin_level, + currency=inf.currency + ) + return AccountInfo(0, 0, 0, 0, 0) + + def get_trade_history(self, days: int = 30) -> List[Dict]: + """Get trade history from MT5""" + from_date = datetime.now() - timedelta(days=days) + history = mt5.history_deals_get(from_date, datetime.now()) + deals = [] + if history: + for d in history: + deals.append({ + "ticket": d.ticket, + "symbol": d.symbol, + "type": d.type, + "volume": d.volume, + "price": d.price, + "profit": d.profit, + "time": datetime.fromtimestamp(d.time) + }) + return deals diff --git a/requirements-mt5.txt b/requirements-mt5.txt new file mode 100644 index 0000000..dcdfb1d --- /dev/null +++ b/requirements-mt5.txt @@ -0,0 +1 @@ +MetaTrader5==5.0.5120 diff --git a/requirements.txt b/requirements.txt index a6bd22d..d4c4eaa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,10 +8,9 @@ idna==3.10 itsdangerous==2.2.0 Jinja2==3.1.6 MarkupSafe==3.0.2 -MetaTrader5==5.0.5120 -numpy==1.23.5 -pandas==2.3.1 -pandas_ta==0.3.14b0 +numpy>=2.2.6 +pandas>=2.2.3 +pandas-ta python-dateutil==2.9.0.post0 python-dotenv==1.1.1 pytz==2025.2 diff --git a/test_agnostic_bot.py b/test_agnostic_bot.py new file mode 100644 index 0000000..bb7e1c6 --- /dev/null +++ b/test_agnostic_bot.py @@ -0,0 +1,142 @@ + +import logging +import pandas as pd +from core.bots.trading_bot import TradingBot +from core.interfaces.broker_interface import BrokerInterface + +# Configure logging +logging.basicConfig(level=logging.INFO) + +class MockBroker(BrokerInterface): + def __init__(self): + self.positions = [] + self._ticket_counter = 1000 + + def initialize(self, credentials): + return True + + def get_account_info(self): + return {'balance': 10000, 'equity': 10000} + + def get_rates(self, symbol, timeframe, count=100): + # Mengembalikan DataFrame dengan format yang tepat + # Dibuat lebih banyak baris (count) untuk simulasi indikator strategi + data = { + 'time': pd.date_range(end=pd.Timestamp.now(), periods=count, freq='H'), + 'open': [1.0] * count, + 'high': [1.1] * count, + 'low': [0.9] * count, + 'close': [1.05] * count, + 'tick_volume': [100] * count + } + return pd.DataFrame(data) + + def get_open_positions(self): + return self.positions + + def place_order(self, symbol: str, order_type: str, volume: float, price: float = 0.0, sl: float = 0.0, tp: float = 0.0, comment: str = ""): + self._ticket_counter += 1 + # Ekstrak magic number dari comment (Bot-ID) agar bot bisa mengenali posisinya + magic = int(comment.split('-')[1]) if 'Bot-' in comment else 0 + + new_pos = { + 'ticket': self._ticket_counter, + 'symbol': symbol, + 'type': order_type, + 'volume': volume, + 'price': price, + 'sl': sl, + 'tp': tp, + 'magic': magic, + 'profit': 10.5 # Dummy profit + } + self.positions.append(new_pos) + print(f"MOCK ORDER PLACED: {new_pos}") + return True + + def close_position(self, position_id, volume: float = 0.0): + # Menghapus posisi dari list internal berdasarkan ticket + self.positions = [p for p in self.positions if str(p['ticket']) != str(position_id)] + print(f"MOCK POSITION CLOSED: ID {position_id}") + return True + + def get_symbol_info(self, symbol): + return {'name': symbol, 'digits': 5} + + def get_todays_profit(self): + return 50.0 + +def test_bot_initialization(): + print("Testing Bot initialization with Mock Broker...") + mock_broker = MockBroker() + bot = TradingBot( + id=999, + name="MockBot", + market="EURUSD", + risk_percent=1.0, + sl_pips=50, + tp_pips=100, + timeframe="H1", + check_interval=1, + strategy="MA_CROSSOVER", # Real strategy from STRATEGY_MAP + broker=mock_broker + ) + + # We won't actually start the thread in this test to avoid loop + print(f"Bot '{bot.name}' initialized successfully with broker {bot.broker.__class__.__name__}") + assert bot.broker == mock_broker + print("Initialization Test passed!") + +def test_full_trade_loop(): + print("\n--- Testing Full Trade Loop (Simulation) ---") + mock_broker = MockBroker() + + # Setup bot + bot = TradingBot( + id=777, + name="LoopTester", + market="EURUSD", + risk_percent=0.1, + sl_pips=50, + tp_pips=100, + timeframe="H1", + check_interval=0.1, + strategy="MA_CROSSOVER", + broker=mock_broker + ) + + # Pre-setup manually for testing internal logic + bot.market_for_mt5 = "EURUSD" + from core.strategies.strategy_map import STRATEGY_MAP + bot.strategy_instance = STRATEGY_MAP["MA_CROSSOVER"](bot_instance=bot) + + # 1. Simulasikan Sinyal BUY + print("\n[Step 1] Simulating BUY Signal...") + # Bot handle signal (no position yet) + bot._handle_trade_signal('BUY', None) + + positions = mock_broker.get_open_positions() + assert len(positions) == 1 + assert positions[0]['type'] == 'BUY' + print(f"Verified: Position opened successfully. Ticket: {positions[0]['ticket']}") + + # 2. Simulasikan Sinyal SELL (Bot harus tutup BUY dulu baru buka SELL) + print("\n[Step 2] Simulating SELL Signal while BUY is open...") + # Get current position like the bot loop does + current_pos = bot._get_open_position() + assert current_pos is not None + + # Trigger signal handler + bot._handle_trade_signal('SELL', current_pos) + + # Verify positions after switch + positions = mock_broker.get_open_positions() + assert len(positions) == 1 + assert positions[0]['type'] == 'SELL' + print("Verification: Old BUY position was closed and new SELL position was opened!") + + print("\nFull Trade Loop Test Passed!") + +if __name__ == "__main__": + test_bot_initialization() + test_full_trade_loop() diff --git a/test_ccxt.py b/test_ccxt.py index c3beed6..29b8055 100644 --- a/test_ccxt.py +++ b/test_ccxt.py @@ -1,49 +1,42 @@ import logging +import os +from dotenv import load_dotenv from core.factory.broker_factory import BrokerFactory # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger("TestCCXT") +load_dotenv() -def test_ccxt(): - logger.info("1. Requesting CCXT adapter from Factory...") +def test_ccxt_minimal(): + # 1. Pastikan env terisi + exchange = os.getenv('EXCHANGE_ID', 'binance') + logger.info(f"Targeting Exchange: {exchange}") + + # 2. Ambil dari Factory (Factory akan otomatis inisialisasi pake .env) try: - broker = BrokerFactory.get_broker('CCXT') - logger.info(" Success: Got CCXTAdapter instance.") - except Exception as e: - logger.error(f" Failed: {e}") - return - - logger.info("2. Initializing connection (Binance Public)...") - # No keys needed for public data - creds = { - 'EXCHANGE_ID': 'binance', - 'API_KEY': '', - 'API_SECRET': '' - } - if broker.initialize(creds): - logger.info(" Success: Connected to Binance.") - else: - logger.error(" Failed: Could not connect.") - return - - logger.info("3. Fetching Rates for BTC/USDT...") - try: - df = broker.get_rates('BTC/USDT', 'H1', 10) - if not df.empty: - logger.info(f" Success: Fetched {len(df)} rows.") - print(df.head()) + broker = BrokerFactory.get_broker('CCXT', exchange_id=exchange) + if broker and broker.exchange: + logger.info(f"โœ… Instance {broker.__class__.__name__} ready.") else: - logger.error(" Failed: DataFrame is empty.") + logger.error("โŒ Failed to get initialized broker instance.") + return except Exception as e: - logger.error(f" Failed: {e}") + logger.error(f"โŒ Factory Error: {e}") + return - logger.info("4. Getting Symbol Info...") - info = broker.get_symbol_info('BTC/USDT') - if info: - logger.info(f" Success: {info}") - else: - logger.error(" Failed: Could not get symbol info.") + # 3. Test Ambil Data (Public) + symbol = 'BTC/USDT' + logger.info(f"Fetching Rates for {symbol}...") + try: + df = broker.get_rates(symbol, 'H1', 5) + if not df.empty: + logger.info("โœ… Profit! Data received:") + print(df[['time', 'close', 'tick_volume']].tail()) + else: + logger.warning("โš ๏ธ Connected, but DataFrame is empty. Check if symbol exists.") + except Exception as e: + logger.error(f"โŒ Request Error: {e}") if __name__ == "__main__": - test_ccxt() + test_ccxt_minimal() \ No newline at end of file diff --git a/test_ccxt_order.py b/test_ccxt_order.py new file mode 100644 index 0000000..b6a1710 --- /dev/null +++ b/test_ccxt_order.py @@ -0,0 +1,64 @@ +import logging +import os +from dotenv import load_dotenv +from core.factory.broker_factory import BrokerFactory + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger("FirstOrder") +load_dotenv() + +def test_place_live_order(): + exchange_id = os.getenv('EXCHANGE_ID', 'binance') + logger.info(f"1. Init Broker: {exchange_id} (Testnet)") + + try: + # BrokerFactory akan inisialisasi pake .env (API Key, Secret, dll) + broker = BrokerFactory.get_broker('CCXT', exchange_id=exchange_id) + + if not broker or not broker.exchange: + logger.error("โŒ Gagal inisialisasi Broker. Cek .env Anda!") + return + + symbol = 'BTC/USDT' + logger.info(f"2. Mengirim Order: BUY {symbol}") + + # Kita coba beli jumlah kecil, misal 0.01 BTC (satuan kontrak di Futures) + success = broker.place_order( + symbol=symbol, + order_type='BUY', + volume=0.01, + comment="Order dari QuantumBotX" + ) + + if success: + logger.info("โœ… BERHASIL! Order telah dieksekusi di Exchange.") + + # Beri jeda sebentar agar data di bursa ter-update + import time + logger.info("Menunggu data posisi terupdate...") + time.sleep(2) + + # Cek daftar posisi terbuka + positions = broker.get_open_positions() + print("\n" + "="*50) + print("POSISI TERBUKA SAAT INI:") + print("="*50) + if not positions: + print("Tidak ada posisi aktif (mungkin langsung tertutup atau error).") + for pos in positions: + side_str = "BUY (Long)" if pos['type'] == 0 else "SELL (Short)" + print(f"- Symbol: {pos['symbol']}") + print(f" Type : {side_str}") + print(f" Volume: {pos['volume']}") + print(f" Price : {pos['price']}") + print(f" Profit: {pos['profit']} USDT") + print("-" * 20) + else: + logger.error("โŒ Gagal menempatkan order. Cek log error di atas.") + + except Exception as e: + logger.error(f"โŒ Terjadi kesalahan fatal: {e}", exc_info=True) + +if __name__ == "__main__": + test_place_live_order() diff --git a/visual_simulation.py b/visual_simulation.py new file mode 100644 index 0000000..0def3db --- /dev/null +++ b/visual_simulation.py @@ -0,0 +1,135 @@ +import logging +import time +import pandas as pd +import random +from core.bots.trading_bot import TradingBot +from core.interfaces.broker_interface import BrokerInterface +from core.strategies.strategy_map import STRATEGY_MAP + +# 1. Setup Logging yang Cantik +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s | %(levelname)-7s | %(message)s', + datefmt='%H:%M:%S' +) +logger = logging.getLogger("QuantumSim") + +# 2. MockBroker yang Lebih Hebat (Bisa update harga) +class AdvancedMockBroker(BrokerInterface): + def __init__(self): + self.positions = [] + self._ticket_counter = 5000 + self.current_price = 88000.0 + self.history = [] + # Generate some initial history + for i in range(100): + self.current_price += random.uniform(-100, 100) + self.history.append(self.current_price) + + def initialize(self, credentials): return True + def get_account_info(self): return {'balance': 100000, 'equity': 100000} + + def get_rates(self, symbol, timeframe, count=100): + # Update harga sedikit setiap kali dipanggil agar simulasi terasa hidup + self.current_price += random.uniform(-150, 150) + self.history.append(self.current_price) + last_prices = self.history[-count:] + + df = pd.DataFrame({ + 'time': pd.date_range(end=pd.Timestamp.now(), periods=len(last_prices), freq='H'), + 'open': [p * 0.999 for p in last_prices], + 'high': [p * 1.002 for p in last_prices], + 'low': [p * 0.998 for p in last_prices], + 'close': last_prices, + 'tick_volume': [random.randint(1000, 5000) for _ in last_prices] + }) + return df + + def get_open_positions(self): return self.positions + + def place_order(self, symbol, order_type, volume, price=0.0, sl=0.0, tp=0.0, comment=""): + self._ticket_counter += 1 + magic = int(comment.split('-')[1]) if 'Bot-' in comment else 0 + new_pos = { + 'ticket': self._ticket_counter, + 'symbol': symbol, + 'type': order_type, + 'volume': volume, + 'price': self.current_price, + 'sl': sl, 'tp': tp, 'magic': magic, + 'profit': 0.0 + } + self.positions.append(new_pos) + logger.info(f"โœจ [BROKER] ORDER BERHASIL: {order_type} {symbol} @ {self.current_price:.2f}") + return True + + def close_position(self, position_id, volume=0.0): + self.positions = [p for p in self.positions if str(p['ticket']) != str(position_id)] + logger.info(f"๐Ÿ›‘ [BROKER] POSISI DITUTUP: ID {position_id}") + return True + + def get_symbol_info(self, symbol): return {'name': symbol, 'digits': 2} + def get_todays_profit(self): return 120.50 + +# 3. Script Simulasi Utama +def run_visual_simulation(): + print("\n" + "="*60) + print(" QUANTUM BOT X - LIVE SIMULATION MODE (MOCK) ") + print("="*60) + + broker = AdvancedMockBroker() + + bot = TradingBot( + id=1337, + name="UltraBot-Sim", + market="BTC/USDT", + risk_percent=0.05, + sl_pips=1000, + tp_pips=2000, + timeframe="H1", + check_interval=2, # Cek setiap 2 detik + strategy="MA_CROSSOVER", # Pake strategi asli + broker=broker + ) + + # Kita jalankan loop bot secara manual agar bisa kita batasi jumlah iterasinya + # (Biasanya bot.start() akan jalan selamanya di thread terpisah) + + # Setup Strategy Instance (biasanya dilakukan di bot.run()) + bot.market_for_mt5 = "BTC/USDT" + bot.strategy_instance = STRATEGY_MAP["MA_CROSSOVER"](bot_instance=bot) + + print(f"Bot '{bot.name}' Ready. Menggunakan Strategi: {bot.strategy_name}") + print("Memulai simulasi 10 iterasi...\n") + + for i in range(1, 11): + print(f"\n--- Iterasi {i}/10 | Harga Saat Ini: {broker.current_price:.2f} ---") + + # Ambil data kandel + df = broker.get_rates(bot.market_for_mt5, bot.timeframe, 50) + + # Analisis Strategi + bot.last_analysis = bot.strategy_instance.analyze(df) + + # PAKSA SINYAL untuk demo agar terlihat log-nya + if i == 2: signal = 'BUY' + elif i == 5: signal = 'SELL' + elif i == 8: signal = 'BUY' + else: signal = bot.last_analysis.get('signal', 'HOLD') + + logger.info(f"Analisis: {signal} | Penjelasan: {bot.last_analysis.get('explanation', 'Manual Override for Demo' if i in [2,5,8] else '')}") + + # Eksekusi (Logika di TradingBot._handle_trade_signal) + posisi_sekarang = bot._get_open_position() + bot._handle_trade_signal(signal, posisi_sekarang) + + # Kasih jeda biar enak dilihat + time.sleep(1.5) + + print("\n" + "="*60) + print(" SIMULASI SELESAI! ") + print("="*60) + print("Bot berhasil mensimulasikan logika trading tanpa menyentuh dana asli.") + +if __name__ == "__main__": + run_visual_simulation()