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()