Files
quantumbotx/core/bots/controller.py
T
Reynov Christian 76df441fbb 🚀 Major Release: Production-Ready QuantumBotX with Advanced Features
 CORE ENHANCEMENTS:
• Beginner-friendly strategy system with educational framework
• ATR-based dynamic risk management with market-adaptive position sizing
• Multi-broker support with automatic symbol migration (XM Global optimized)
• Advanced crypto trading strategies (SatoshiJakarta & QuantumCrypto bots)
• Ultra-conservative XAUUSD protection system preventing account blowouts

🛡️ SAFETY & RISK MANAGEMENT:
• Dynamic position sizing based on market volatility (ATR)
• Emergency brake system for dangerous trades
• Progressive learning path for beginners (Week 1-6 curriculum)
• Strategy complexity ratings (2-12 scale) with difficulty-based recommendations
• Special gold trading protection with fixed lot sizes

🎓 EDUCATIONAL FEATURES:
• Strategy selector with automatic recommendations by experience level
• Parameter validation with beginner-safe warnings
• Educational explanations for every trading parameter
• Market-specific strategy suggestions (FOREX vs GOLD vs CRYPTO)
• Complete learning framework from beginner to expert

🔧 TECHNICAL IMPROVEMENTS:
• Enhanced backtesting engine with comprehensive history tracking
• Quiet logging system (user preference for clean terminal output)
• Robust error handling and Windows compatibility fixes
• Multi-timeframe analysis support across all strategies
• Real-time market data integration with broker detection

📊 NEW STRATEGIES:
• QuantumBotX Crypto: Bitcoin-optimized with weekend trading mode
• Enhanced Hybrid: Auto-detects crypto vs forex for optimal parameters
• Beginner-friendly MA Crossover with educational defaults
• Advanced multi-indicator strategies (Mercy Edge, Pulse Sync)

🌐 PLATFORM EXPANSION:
• Indonesian market integration planning (XM Indonesia support)
• Multi-broker architecture foundation (cTrader, Interactive Brokers)
• Comprehensive testing suite with 15+ validation scripts
• Professional documentation and troubleshooting guides

📈 BETA READINESS:
• Production-grade stability with 4 concurrent trading bots
• Professional UI/UX with real-time performance tracking
• Comprehensive error handling and user guidance
• Windows-optimized deployment with MT5 integration

Score: 10/10 Production Ready! 🏆
2025-08-25 23:14:43 +08:00

259 lines
10 KiB
Python

# core/bots/controller.py
import json
import logging
from core.db import queries
from .trading_bot import TradingBot
logger = logging.getLogger(__name__)
# Dictionary untuk menyimpan instance thread bot yang aktif
# Key: bot_id (int), Value: TradingBot instance
active_bots = {}
def auto_migrate_broker_symbols():
"""Automatically migrate bot symbols when broker changes are detected"""
try:
import MetaTrader5 as mt5
from pathlib import Path
import json
from core.utils.mt5 import find_mt5_symbol
# Get current broker info
account_info = mt5.account_info()
if not account_info:
return
current_broker = account_info.server
broker_file = Path('last_broker.json')
# Check if broker changed
broker_changed = False
if broker_file.exists():
with open(broker_file, 'r') as f:
last_config = json.load(f)
last_broker = last_config.get('broker', '')
if last_broker != current_broker:
logger.info(f"Broker changed detected: '{last_broker}' → '{current_broker}'")
broker_changed = True
else:
broker_changed = True # First time setup
if broker_changed:
logger.info("Running automatic symbol migration...")
# Get all bots and check symbols
all_bots = queries.get_all_bots()
migrated_count = 0
for bot in all_bots:
bot_id = bot['id']
current_symbol = bot['market']
# Test current symbol
resolved_symbol = find_mt5_symbol(current_symbol)
if resolved_symbol and resolved_symbol != current_symbol:
# Symbol needs updating
logger.info(f"Auto-migrating Bot {bot_id} ({bot['name']}): {current_symbol} -> {resolved_symbol}")
# Preserve all existing bot settings, only change symbol
success = queries.update_bot(
bot_id=bot_id,
name=bot['name'],
market=resolved_symbol, # Only change this
lot_size=bot['lot_size'],
sl_pips=bot['sl_pips'],
tp_pips=bot['tp_pips'],
timeframe=bot['timeframe'],
interval=bot['check_interval_seconds'],
strategy=bot['strategy'],
strategy_params=bot['strategy_params'] or '{}'
)
if success:
migrated_count += 1
elif not resolved_symbol:
logger.warning(f"Bot {bot_id} ({bot['name']}) symbol '{current_symbol}' not available on {current_broker}")
logger.info(f"Auto-migration complete: {migrated_count} bots updated for {current_broker}")
# Save current broker info
with open(broker_file, 'w') as f:
json.dump({
'broker': current_broker,
'company': account_info.company,
'last_check': __import__('datetime').datetime.now().isoformat()
}, f, indent=2)
except Exception as e:
logger.error(f"Error in auto symbol migration: {e}")
def ambil_semua_bot():
"""
Mengambil semua bot dari database saat aplikasi pertama kali dimulai.
Automatically handles broker symbol migration before loading bots.
"""
try:
# First, auto-migrate symbols if broker changed
auto_migrate_broker_symbols()
all_bots_data = queries.get_all_bots()
if not all_bots_data:
logger.info("Database tidak memiliki bot untuk dimuat.")
return
logger.info(f"Memuat {len(all_bots_data)} bot dari database. Memeriksa bot yang perlu diaktifkan ulang...")
for bot_data in all_bots_data:
if bot_data.get('status') == 'Aktif':
logger.info(f"Bot ID {bot_data['id']} ({bot_data['name']}) memiliki status 'Aktif'. Mencoba memulai ulang...")
mulai_bot(bot_data['id'])
except Exception as e:
logger.error(f"Gagal memuat bot dari database saat startup: {e}", exc_info=True)
def mulai_bot(bot_id: int):
"""Memulai thread untuk bot yang dipilih."""
if bot_id in active_bots and active_bots[bot_id].is_alive():
return True, f"Bot {bot_id} sudah berjalan."
bot_data = queries.get_bot_by_id(bot_id)
if not bot_data:
return False, f"Bot dengan ID {bot_id} tidak ditemukan."
# Ubah string JSON dari DB menjadi dictionary Python
params_dict = json.loads(bot_data.get('strategy_params', '{}'))
try:
bot_thread = TradingBot(
id=bot_data['id'], name=bot_data['name'], market=bot_data['market'],
risk_percent=bot_data['lot_size'], sl_pips=bot_data['sl_pips'],
tp_pips=bot_data['tp_pips'], timeframe=bot_data['timeframe'],
check_interval=bot_data['check_interval_seconds'], strategy=bot_data['strategy'],
strategy_params=params_dict
)
bot_thread.start()
active_bots[bot_id] = bot_thread
queries.update_bot_status(bot_id, 'Aktif')
logger.info(f"Bot {bot_id} ({bot_data['name']}) berhasil dimulai.")
return True, f"Bot {bot_data['name']} berhasil dimulai."
except Exception as e:
logger.error(f"Gagal memulai bot {bot_id}: {e}", exc_info=True)
queries.update_bot_status(bot_id, 'Error')
return False, f"Gagal memulai bot: {e}"
def hentikan_bot(bot_id: int):
"""Menghentikan thread bot yang sedang berjalan."""
# PERBAIKAN: Gunakan .pop() untuk mengambil dan menghapus bot secara atomik.
# Ini mencegah race condition di mana dua proses mencoba menghentikan bot yang sama.
bot_thread = active_bots.pop(bot_id, None)
if bot_thread and bot_thread.is_alive():
bot_thread.stop()
bot_thread.join(timeout=10) # Tunggu thread berhenti
queries.update_bot_status(bot_id, 'Dijeda')
logger.info(f"Bot {bot_id} berhasil dihentikan.")
return True, f"Bot {bot_thread.name} berhasil dihentikan."
# Jika bot tidak ada di memori (mungkin sudah dihentikan oleh proses lain)
# atau jika status di DB tidak konsisten, pastikan status di DB benar.
queries.update_bot_status(bot_id, 'Dijeda') # Pastikan status di DB adalah 'Dijeda'
return True, f"Bot {bot_id} sudah dihentikan atau tidak sedang berjalan."
def mulai_semua_bot():
"""Memulai semua bot yang statusnya 'Dijeda'."""
all_bots = queries.get_all_bots()
bots_to_start = [bot for bot in all_bots if bot['status'] == 'Dijeda']
if not bots_to_start:
return False, "Tidak ada bot yang bisa dimulai (semua sudah aktif atau error)."
started_count = 0
for bot in bots_to_start:
success, _ = mulai_bot(bot['id'])
if success:
started_count += 1
return True, f"Berhasil memulai {started_count} dari {len(bots_to_start)} bot."
def hentikan_semua_bot():
"""Menghentikan semua bot yang sedang berjalan."""
running_bot_ids = list(active_bots.keys())
if not running_bot_ids:
return False, "Tidak ada bot yang sedang berjalan."
for bot_id in running_bot_ids:
hentikan_bot(bot_id)
return True, f"Sinyal berhenti telah dikirim ke {len(running_bot_ids)} bot."
# Alias untuk dipanggil oleh atexit di run.py, memastikan nama sesuai.
shutdown_all_bots = hentikan_semua_bot
def perbarui_bot(bot_id: int, data: dict):
"""Memperbarui konfigurasi bot di database."""
bot_instance = active_bots.get(bot_id)
if bot_instance and bot_instance.is_alive():
logger.info(f"Menghentikan bot {bot_id} sementara untuk pembaruan.")
hentikan_bot(bot_id)
# --- PERBAIKAN DI SINI ---
# Ganti nama kunci 'check_interval_seconds' dari frontend
# menjadi 'interval' yang sesuai dengan kolom database.
if 'check_interval_seconds' in data:
data['interval'] = data.pop('check_interval_seconds')
# --- PERBAIKAN BARU: Terjemahkan input ATR Multiplier ke kolom pips ---
if 'sl_atr_multiplier' in data:
data['sl_pips'] = data.pop('sl_atr_multiplier')
if 'tp_atr_multiplier' in data:
data['tp_pips'] = data.pop('tp_atr_multiplier')
if 'risk_percent' in data:
data['lot_size'] = data.pop('risk_percent')
# Ambil parameter kustom, ubah jadi string JSON, dan simpan
custom_params = data.pop('params', {})
data['strategy_params'] = json.dumps(custom_params)
# --- PERBAIKAN BARU: Filter data untuk mencegah TypeError ---
# Hanya teruskan argumen yang diharapkan oleh fungsi queries.update_bot
expected_args = [
'name', 'market', 'lot_size', 'sl_pips', 'tp_pips',
'timeframe', 'interval', 'strategy', 'strategy_params'
]
update_data = {key: data[key] for key in expected_args if key in data}
try:
# Gunakan dictionary yang sudah difilter
success = queries.update_bot(bot_id=bot_id, **update_data)
if success:
logger.info(f"Konfigurasi bot {bot_id} berhasil diperbarui di database.")
return True, "Bot berhasil diperbarui."
else:
return False, "Gagal memperbarui bot di database."
except Exception as e:
logger.error(f"Error saat memperbarui bot {bot_id} di DB: {e}", exc_info=True)
return False, str(e)
def hapus_bot(bot_id: int):
"""Menghentikan dan menghapus bot."""
hentikan_bot(bot_id) # Pastikan thread berhenti sebelum dihapus
return queries.delete_bot(bot_id)
def add_new_bot_to_controller(bot_id: int):
"""Menambahkan bot baru dan langsung memulainya jika statusnya 'Aktif'."""
bot_data = queries.get_bot_by_id(bot_id)
if bot_data and bot_data.get('status') == 'Aktif':
mulai_bot(bot_id)
def get_bot_instance_by_id(bot_id: int):
"""Mengembalikan instance thread bot yang aktif."""
return active_bots.get(bot_id)
def get_bot_analysis_data(bot_id: int):
"""Mengambil data analisis terakhir dari instance bot."""
bot = active_bots.get(bot_id)
if bot and hasattr(bot, 'last_analysis'):
return bot.last_analysis
return None