Files
quantumbotx/core/bots/trading_bot.py
T
Reynov Christian 73fe9cdca2 feat: Add Quantum Velocity strategy and major stability overhaul" -m "
This major commit introduces a new advanced trading strategy, resolves critical trading logic errors, and significantly enhances system stability and UI data accuracy.

Key Changes:

- **New Features & Strategies**:
  - **Quantum Velocity Strategy**: Added 'quantum_velocity.py', a new hybrid strategy combining a long-term trend filter (EMA 200) with a volatility-based entry trigger (Bollinger Squeeze).
  - **Hybrid Pro Backtester**: Added 'lab/backtester_hybrid_pro.py' to facilitate advanced, offline testing and optimization of the QuantumBotX Hybrid strategy.

- **Critical Trading Logic Fixes**:
  - Resolved 'Invalid Stops' and 'Unsupported filling mode' errors by refactoring 'core/mt5/trade.py' to use dynamic point calculation and the FOK fill policy.
  - Fixed a recurring 'KeyError' in 'BollingerSqueezeStrategy' by ensuring consistent naming for Bollinger Bands columns.

- **System Stability & Robustness**:
  - Hardened the graceful shutdown handler in 'app.py' to prevent crashes from repeated Ctrl+C signals, ensuring a clean shutdown process.
  - Enhanced the notification system by adding an 'is_notification' flag to logs, allowing for a clear distinction between critical alerts and general activity.

- **Dashboard & UI Enhancements**:
  - Fixed the 'Total Bots' card on the dashboard by correcting the backend API ('api_dashboard.py') to send the complete stats payload.
  - Replaced static '0' values on dashboard cards with loading spinners for a more professional user experience.
  - The 'Create/Edit Bot' modal button now dynamically changes its text to 'Ubah Bot' when in edit mode.
2025-08-01 22:47:37 +08:00

150 lines
6.6 KiB
Python

# core/bots/trading_bot.py - VERSI GABUNGAN FINAL
import threading
import time
import logging
import MetaTrader5 as mt5
from core.strategies.strategy_map import STRATEGY_MAP
from core.mt5.trade import place_trade, close_trade
from core.utils.mt5 import TIMEFRAME_MAP # <-- Impor dari lokasi terpusat
logger = logging.getLogger(__name__)
class TradingBot(threading.Thread):
def __init__(self, id, name, market, lot_size, sl_pips, tp_pips, timeframe, check_interval, strategy, strategy_params={}, status='Dijeda'):
super().__init__()
self.id = id
self.name = name
self.market = market
self.lot_size = lot_size
self.sl_pips = sl_pips
self.tp_pips = tp_pips
self.timeframe = timeframe
self.check_interval = check_interval
self.strategy_name = strategy
self.strategy_params = strategy_params
self.market_for_mt5 = self.market.replace('/', '') # Versi simbol yang bersih untuk MT5
self.status = status
self.last_analysis = {}
self._stop_event = threading.Event()
self.strategy_instance = None
# Gunakan map yang diimpor untuk menjaga konsistensi
self.tf_map = TIMEFRAME_MAP
def run(self):
"""Metode utama yang dijalankan oleh thread, kini dengan eksekusi trade."""
self.status = 'Aktif'
self.log_activity('START', f"Bot '{self.name}' dimulai.", is_notification=True)
try:
strategy_class = STRATEGY_MAP.get(self.strategy_name)
if not strategy_class:
raise ValueError(f"Strategi '{self.strategy_name}' tidak ditemukan.")
# --- PERBAIKAN: Inisialisasi kelas strategi dengan benar ---
self.strategy_instance = strategy_class(bot_instance=self, params=self.strategy_params)
except Exception as e:
self.log_activity('ERROR', f"Inisialisasi Gagal: {e}", is_notification=True)
self.status = 'Error'
return
while not self._stop_event.is_set():
try:
if not mt5.symbol_select(self.market_for_mt5, True):
msg = f"Gagal mengaktifkan simbol {self.market} (di MT5: {self.market_for_mt5}). Pastikan simbol ada di Market Watch."
self.log_activity('WARNING', msg)
self.last_analysis = {"signal": "ERROR", "price": None, "explanation": msg}
time.sleep(self.check_interval)
continue
symbol_info = mt5.symbol_info(self.market_for_mt5)
if not symbol_info:
msg = f"Tidak dapat mengambil info untuk simbol {self.market} (di MT5: {self.market_for_mt5})."
self.log_activity('WARNING', msg)
self.last_analysis = {"signal": "ERROR", "price": None, "explanation": msg}
time.sleep(self.check_interval)
continue
# --- PERBAIKAN: Panggil metode analyze dari objek strategi ---
# Metode analyze tidak lagi memerlukan argumen
self.last_analysis = self.strategy_instance.analyze()
logger.info(f"Bot {self.id} [{self.strategy_name}] - Last Analysis: {self.last_analysis}")
signal = self.last_analysis.get("signal", "HOLD")
current_position = self._get_open_position()
self._handle_trade_signal(signal, current_position)
time.sleep(self.check_interval)
except Exception as e:
self.log_activity('ERROR', f"Error pada loop utama: {e}", exc_info=True, is_notification=True)
time.sleep(self.check_interval * 2)
self.status = 'Dijeda'
self.log_activity('STOP', f"Bot '{self.name}' dihentikan.", is_notification=True)
def stop(self):
"""Mengirim sinyal berhenti ke thread."""
self._stop_event.set()
def is_stopped(self):
"""Memeriksa apakah thread sudah diberi sinyal berhenti."""
return self._stop_event.is_set()
def log_activity(self, action, details, exc_info=False, is_notification=False):
"""Mencatat aktivitas bot ke database dan file log."""
try:
from core.db.queries import add_history_log
add_history_log(self.id, action, details, is_notification)
log_message = f"Bot {self.id} [{action}]: {details}"
if exc_info:
logger.error(log_message, exc_info=True)
else:
logger.info(log_message)
except Exception as e:
logger.error(f"Gagal mencatat riwayat untuk bot {self.id}: {e}")
def _get_open_position(self):
"""Mendapatkan posisi terbuka untuk bot ini berdasarkan magic number (ID bot)."""
try:
positions = mt5.positions_get(symbol=self.market_for_mt5)
if positions:
for pos in positions:
if pos.magic == self.id:
return pos
return None
except Exception as e:
self.log_activity('ERROR', f"Gagal mendapatkan posisi terbuka: {e}", exc_info=True, is_notification=True)
return None
def _handle_trade_signal(self, signal, position):
"""Menangani sinyal trading: membuka, menutup, atau tidak melakukan apa-apa."""
# Logika untuk sinyal BUY
if signal == 'BUY':
# Jika ada posisi SELL, tutup dulu
if position and position.type == mt5.ORDER_TYPE_SELL:
self.log_activity('CLOSE SELL', "Menutup posisi JUAL untuk membuka posisi BELI.", is_notification=True)
close_trade(position)
position = None # Reset posisi setelah ditutup
# Jika tidak ada posisi, buka posisi BUY baru
if not position:
self.log_activity('OPEN BUY', "Membuka posisi BELI berdasarkan sinyal.", is_notification=True)
place_trade(self.market_for_mt5, mt5.ORDER_TYPE_BUY, self.lot_size, self.sl_pips, self.tp_pips, self.id)
# Logika untuk sinyal SELL
elif signal == 'SELL':
# Jika ada posisi BUY, tutup dulu
if position and position.type == mt5.ORDER_TYPE_BUY:
self.log_activity('CLOSE BUY', "Menutup posisi BELI untuk membuka posisi JUAL.", is_notification=True)
close_trade(position)
position = None # Reset posisi setelah ditutup
# Jika tidak ada posisi, buka posisi SELL baru
if not position:
self.log_activity('OPEN SELL', "Membuka posisi JUAL berdasarkan sinyal.", is_notification=True)
place_trade(self.market_for_mt5, mt5.ORDER_TYPE_SELL, self.lot_size, self.sl_pips, self.tp_pips, self.id)