mirror of
https://github.com/chrisnov-it/quantumbotx.git
synced 2026-07-28 11:17:44 +00:00
67734f926b
✨ Major Features Added: • Complete Indonesian & English bilingual support (80+ UI elements) • Global language switcher with persistent preferences • Smart MT5 data integration with web controls • One-click market data download for 50+ instruments • Automated MT5 installer and setup guide • Enhanced settings with profile and preferences management • Professional documentation and troubleshooting guides 🚀 Technical Improvements: • Internationalization (i18n) system with extensible architecture • MT5 environment detection with graceful fallbacks • API architecture for future cross-platform expansion • Improved error handling and user guidance • Cross-browser compatibility and responsive design 📚 Documentation: • 50+ page comprehensive MT5 setup guide • Automated installer with dependency management • Public roadmap announcing QuantumBotX API development • Updated CHANGELOG and platform compatibility notices 🎯 Platform Strategy: • Clear Windows-first positioning for MT5 learning platform • Preparatory work for QuantumBotX API cloud platform • Education-first approach maintaining Indonesian heritage • Foundation for international expansion Co-authored-by: AI Assistant <assistant@quantumbotx.ai>
300 lines
12 KiB
Python
300 lines
12 KiB
Python
# core/bots/controller.py
|
|
|
|
import json
|
|
import logging
|
|
from core.db import queries
|
|
from .trading_bot import TradingBot
|
|
from core.strategies.strategy_map import STRATEGY_MAP
|
|
|
|
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() # pyright: ignore[reportAttributeAccessIssue]
|
|
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,
|
|
enable_strategy_switching=bool(bot_data.get('enable_strategy_switching', 0))
|
|
)
|
|
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
|
|
|
|
# For inactive bots, generate analysis on-demand
|
|
bot_data = queries.get_bot_by_id(bot_id)
|
|
if not bot_data:
|
|
return None
|
|
|
|
try:
|
|
import MetaTrader5 as mt5
|
|
from core.utils.mt5 import find_mt5_symbol, get_rates_mt5, TIMEFRAME_MAP
|
|
|
|
# Find the symbol
|
|
market_for_mt5 = find_mt5_symbol(bot_data['market'])
|
|
if not market_for_mt5:
|
|
return {"signal": "ERROR", "explanation": f"Symbol '{bot_data['market']}' not found in MT5"}
|
|
|
|
# Get market data
|
|
tf_const = TIMEFRAME_MAP.get(bot_data['timeframe'], mt5.TIMEFRAME_H1)
|
|
df = get_rates_mt5(market_for_mt5, tf_const, 250)
|
|
if df.empty:
|
|
return {"signal": "ERROR", "explanation": "Unable to fetch market data"}
|
|
|
|
# Instantiate strategy
|
|
strategy_class = STRATEGY_MAP.get(bot_data['strategy'])
|
|
if not strategy_class:
|
|
return {"signal": "ERROR", "explanation": f"Strategy '{bot_data['strategy']}' not found"}
|
|
|
|
# Parse strategy params
|
|
params_dict = json.loads(bot_data.get('strategy_params', '{}'))
|
|
|
|
# Create a temp strategy instance (without bot_instance since it's just for analysis)
|
|
strategy_instance = strategy_class(bot_instance=None, params=params_dict)
|
|
|
|
# Generate analysis
|
|
analysis = strategy_instance.analyze(df)
|
|
return analysis
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error generating analysis for inactive bot {bot_id}: {e}")
|
|
return {"signal": "ERROR", "explanation": f"Failed to generate analysis: {str(e)}"}
|