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.
This commit is contained in:
Reynov Christian
2025-08-01 22:47:37 +08:00
parent 33e55d12bb
commit 73fe9cdca2
16 changed files with 447 additions and 247 deletions
+1
View File
@@ -127,6 +127,7 @@ Concept, Logic & Execution: `@reynov` aka BabyDev
If you like this project, give it a ⭐ on GitHub, or buy me a coffee to support future versions:
// eslint-disable-next-line markdown/fenced-code-language
```
BTC Wallet: bc1qxxxxxxxxxxxxxx
USDT TRC20: TRxxxxxxxxxxxx
+15 -4
View File
@@ -128,11 +128,22 @@ def shutdown_handler():
# Hentikan semua bot yang aktif
active_bot_ids = list(controller.active_bots.keys())
for bot_id in active_bot_ids:
controller.stop_bot(bot_id)
if active_bot_ids:
logger.info(f"Menghentikan {len(active_bot_ids)} bot yang aktif...")
mt5.shutdown()
logger.info("Koneksi MetaTrader 5 ditutup. Shutdown selesai.")
for bot_id in active_bot_ids:
try:
logger.info(f"Memberi sinyal berhenti untuk bot ID {bot_id}...")
controller.stop_bot(bot_id)
except KeyboardInterrupt:
# Abaikan Ctrl+C tambahan saat proses shutdown sedang berlangsung.
logger.warning(f"KeyboardInterrupt diterima saat menghentikan bot {bot_id}. Melanjutkan proses shutdown.")
continue
except Exception as e:
logger.error(f"Error tak terduga saat menghentikan bot {bot_id} selama shutdown: {e}")
mt5.shutdown() # Pastikan koneksi MT5 selalu ditutup
logger.info("Koneksi MetaTrader 5 ditutup. Proses shutdown selesai.")
# --- Titik Eksekusi Utama ---
if __name__ == '__main__':
+11 -11
View File
@@ -36,7 +36,7 @@ class TradingBot(threading.Thread):
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 dengan strategi {self.strategy_name}.") # <-- Menggunakan log_activity
self.log_activity('START', f"Bot '{self.name}' dimulai.", is_notification=True)
try:
strategy_class = STRATEGY_MAP.get(self.strategy_name)
@@ -47,7 +47,7 @@ class TradingBot(threading.Thread):
self.strategy_instance = strategy_class(bot_instance=self, params=self.strategy_params)
except Exception as e:
self.log_activity('ERROR', f"Inisialisasi Gagal: {e}")
self.log_activity('ERROR', f"Inisialisasi Gagal: {e}", is_notification=True)
self.status = 'Error'
return
@@ -80,11 +80,11 @@ class TradingBot(threading.Thread):
time.sleep(self.check_interval)
except Exception as e:
self.log_activity('ERROR', f"Error pada loop utama: {e}", exc_info=True)
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.")
self.log_activity('STOP', f"Bot '{self.name}' dihentikan.", is_notification=True)
def stop(self):
"""Mengirim sinyal berhenti ke thread."""
@@ -94,11 +94,11 @@ class TradingBot(threading.Thread):
"""Memeriksa apakah thread sudah diberi sinyal berhenti."""
return self._stop_event.is_set()
def log_activity(self, action, details, exc_info=False):
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)
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)
@@ -117,7 +117,7 @@ class TradingBot(threading.Thread):
return pos
return None
except Exception as e:
self.log_activity('ERROR', f"Gagal mendapatkan posisi terbuka: {e}", exc_info=True)
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):
@@ -126,24 +126,24 @@ class TradingBot(threading.Thread):
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.")
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.")
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.")
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.")
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)
+36 -4
View File
@@ -77,13 +77,13 @@ def update_bot_status(bot_id, status):
except sqlite3.Error as e:
logger.error(f"Gagal update status bot {bot_id}: {e}")
def add_history_log(bot_id, action, details):
def add_history_log(bot_id, action, details, is_notification=False):
"""Menambahkan log aktivitas/riwayat untuk bot tertentu."""
try:
with get_db_connection() as conn:
conn.execute(
'INSERT INTO trade_history (bot_id, action, details) VALUES (?, ?, ?)',
(bot_id, action, details)
'INSERT INTO trade_history (bot_id, action, details, is_notification, is_read) VALUES (?, ?, ?, ?, ?)',
(bot_id, action, details, is_notification, False) # is_read selalu False saat dibuat
)
conn.commit()
except sqlite3.Error as e:
@@ -100,4 +100,36 @@ def get_history_by_bot_id(bot_id):
return [dict(row) for row in history]
except sqlite3.Error as e:
logger.error(f"Database error saat mengambil riwayat bot {bot_id}: {e}")
return []
return []
def get_notifications():
"""Mengambil semua log yang ditandai sebagai notifikasi."""
try:
with get_db_connection() as conn:
notifications = conn.execute('''
SELECT h.id, h.action, h.details, h.is_read, h.timestamp, b.name as bot_name
FROM trade_history h
JOIN bots b ON h.bot_id = b.id
WHERE h.is_notification = 1
ORDER BY h.timestamp DESC
''').fetchall()
return [dict(row) for row in notifications]
except sqlite3.Error as e:
logger.error(f"Database error saat mengambil notifikasi: {e}")
return []
def get_unread_notifications_count():
"""Menghitung jumlah notifikasi yang belum dibaca."""
try:
with get_db_connection() as conn:
count = conn.execute('SELECT COUNT(id) as unread_count FROM trade_history WHERE is_notification = 1 AND is_read = 0').fetchone()
return dict(count) if count else {'unread_count': 0}
except sqlite3.Error as e:
logger.error(f"Database error saat menghitung notifikasi: {e}")
return {'unread_count': 0}
def mark_notifications_as_read():
"""Menandai semua notifikasi sebagai sudah dibaca."""
with get_db_connection() as conn:
conn.execute('UPDATE trade_history SET is_read = 1 WHERE is_notification = 1 AND is_read = 0')
conn.commit()
+81 -93
View File
@@ -1,116 +1,104 @@
# core/mt5/trade.py
import MetaTrader5 as mt5
import logging
import MetaTrader5 as mt5
logger = logging.getLogger(__name__)
def place_trade(symbol, order_type, volume, sl_pips, tp_pips, magic):
def place_trade(symbol, order_type, volume, sl_pips, tp_pips, magic_id):
"""
Menempatkan order trading ke MetaTrader 5 dengan logika yang lebih tangguh.
Menempatkan trade dengan perhitungan SL/TP yang dinamis berdasarkan 'point' simbol.
"""
symbol_info = mt5.symbol_info(symbol)
if symbol_info is None:
logger.error(f"Gagal mendapatkan info untuk simbol {symbol}, order dibatalkan.")
return None
try:
# 1. Dapatkan informasi simbol untuk point dan digits
symbol_info = mt5.symbol_info(symbol)
if symbol_info is None:
logger.error(f"Gagal mendapatkan info untuk simbol {symbol}. Order dibatalkan.")
return None, "Symbol not found"
# Ambil filling mode yang diizinkan oleh simbol
allowed_filling_mode = symbol_info.filling_mode
point = symbol_info.point
digits = symbol_info.digits
# --- PERBAIKAN: Gunakan nilai literal untuk kompatibilitas ---
# Beberapa versi library MT5 tidak memiliki konstanta SYMBOL_FILLING_*.
# Menggunakan nilai integer langsung lebih aman dan kompatibel mundur.
# FOK_MODE_FLAG = 1
# IOC_MODE_FLAG = 2
# 2. Tentukan harga entry (ask untuk BUY, bid untuk SELL)
if order_type == mt5.ORDER_TYPE_BUY:
price = mt5.symbol_info_tick(symbol).ask
elif order_type == mt5.ORDER_TYPE_SELL:
price = mt5.symbol_info_tick(symbol).bid
else:
logger.error(f"Tipe order tidak valid: {order_type}")
return None, "Invalid order type"
filling_type = mt5.ORDER_FILLING_FOK # Default
# Prioritaskan IOC jika didukung oleh broker untuk simbol ini
if allowed_filling_mode & 2: # Cek flag untuk IOC (nilai 2)
filling_type = mt5.ORDER_FILLING_IOC
# Jika tidak, gunakan FOK jika didukung
elif allowed_filling_mode & 1: # Cek flag untuk FOK (nilai 1)
filling_type = mt5.ORDER_FILLING_FOK
else:
logger.warning(f"Simbol {symbol} tidak mendukung FOK atau IOC. Menggunakan FOK sebagai fallback.")
# 3. Hitung SL dan TP berdasarkan pips dan point
# Asumsi umum: 1 pip = 10 points. Ini membuat kode konsisten di semua pair.
pip_value = 10 * point
sl_level = 0.0
tp_level = 0.0
point = symbol_info.point
price = mt5.symbol_info_tick(symbol).ask if order_type == mt5.ORDER_TYPE_BUY else mt5.symbol_info_tick(symbol).bid
if order_type == mt5.ORDER_TYPE_BUY:
sl_level = price - (sl_pips * pip_value)
tp_level = price + (tp_pips * pip_value)
elif order_type == mt5.ORDER_TYPE_SELL:
sl_level = price + (sl_pips * pip_value)
tp_level = price - (tp_pips * pip_value)
# Bulatkan ke jumlah digit yang benar untuk menghindari error presisi
sl_level = round(sl_level, digits)
tp_level = round(tp_level, digits)
sl = 0.0
tp = 0.0
# 4. Siapkan request order
request = {
"action": mt5.TRADE_ACTION_DEAL,
"symbol": symbol,
"volume": float(volume),
"type": order_type,
"price": price,
"sl": sl_level,
"tp": tp_level,
"magic": magic_id,
"comment": "QuantumBotX Trade",
"type_time": mt5.ORDER_TIME_GTC,
"type_filling": mt5.ORDER_FILLING_FOK, # FOK lebih umum didukung oleh broker ECN
}
if order_type == mt5.ORDER_TYPE_BUY:
sl = price - sl_pips * point if sl_pips > 0 else 0.0
tp = price + tp_pips * point if tp_pips > 0 else 0.0
elif order_type == mt5.ORDER_TYPE_SELL:
sl = price + sl_pips * point if sl_pips > 0 else 0.0
tp = price - tp_pips * point if tp_pips > 0 else 0.0
# 5. Kirim order
result = mt5.order_send(request)
request = {
"action": mt5.TRADE_ACTION_DEAL,
"symbol": symbol,
"volume": float(volume),
"type": order_type,
"price": price,
"sl": sl,
"tp": tp,
"magic": magic,
"comment": "QuantumBotX Trade",
"type_time": mt5.ORDER_TIME_GTC,
"type_filling": filling_type,
}
logger.info(f"Mengirim order: {request}")
result = mt5.order_send(request)
if result is None:
logger.error(f"order_send gagal, last_error()={mt5.last_error()}")
return None
if result.retcode != mt5.TRADE_RETCODE_DONE:
logger.error(f"Order GAGAL, retcode={result.retcode}, comment: {result.comment}")
logger.error(f"Request Gagal: {request}")
else:
logger.info(f"Order BERHASIL, ticket={result.order}, comment: {result.comment}")
return result
# 6. Cek hasil dan log
if result.retcode != mt5.TRADE_RETCODE_DONE:
logger.error(f"Order GAGAL, retcode={result.retcode}, comment: {result.comment}")
logger.error(f"Request Gagal: {request}")
return None, result.comment
logger.info(f"Order BERHASIL ditempatkan: Deal #{result.deal}, Order #{result.order}")
return result, "Order placed successfully"
except Exception as e:
logger.error(f"Exception saat menempatkan trade: {e}", exc_info=True)
return None, str(e)
def close_trade(position):
"""
Menutup posisi trading yang ada.
Menutup posisi yang ada.
"""
if position is None:
return
try:
close_order_type = mt5.ORDER_TYPE_SELL if position.type == mt5.ORDER_TYPE_BUY else mt5.ORDER_TYPE_BUY
price = mt5.symbol_info_tick(position.symbol).bid if close_order_type == mt5.ORDER_TYPE_SELL else mt5.symbol_info_tick(position.symbol).ask
symbol = position.symbol
ticket = position.ticket
volume = position.volume
order_type = mt5.ORDER_TYPE_SELL if position.type == mt5.ORDER_TYPE_BUY else mt5.ORDER_TYPE_BUY
price = mt5.symbol_info_tick(symbol).bid if position.type == mt5.ORDER_TYPE_BUY else mt5.symbol_info_tick(symbol).ask
request = {
"action": mt5.TRADE_ACTION_DEAL, "position": position.ticket, "symbol": position.symbol,
"volume": position.volume, "type": close_order_type, "price": price, "magic": position.magic,
"comment": "QuantumBotX Close", "type_time": mt5.ORDER_TIME_GTC, "type_filling": mt5.ORDER_FILLING_FOK,
}
symbol_info = mt5.symbol_info(symbol)
if symbol_info is None:
logger.error(f"Gagal mendapatkan info untuk simbol {symbol} saat menutup posisi, order dibatalkan.")
return None
allowed_filling_mode = symbol_info.filling_mode
# Terapkan logika yang sama untuk menutup posisi
filling_type = mt5.ORDER_FILLING_FOK
if allowed_filling_mode & 2: # Cek flag untuk IOC (nilai 2)
filling_type = mt5.ORDER_FILLING_IOC
elif allowed_filling_mode & 1: # Cek flag untuk FOK (nilai 1)
filling_type = mt5.ORDER_FILLING_FOK
result = mt5.order_send(request)
if result.retcode != mt5.TRADE_RETCODE_DONE:
logger.error(f"Gagal menutup posisi #{position.ticket}, retcode={result.retcode}, comment: {result.comment}")
return None, result.comment
request = {
"action": mt5.TRADE_ACTION_DEAL, "symbol": symbol, "volume": float(volume),
"type": order_type, "position": ticket, "price": price, "magic": position.magic,
"comment": "QuantumBotX Close", "type_time": mt5.ORDER_TIME_GTC, "type_filling": filling_type,
}
logger.info(f"Posisi #{position.ticket} berhasil ditutup.")
return result, "Position closed successfully"
logger.info(f"Menutup posisi: {request}")
result = mt5.order_send(request)
if result and result.retcode == mt5.TRADE_RETCODE_DONE:
logger.info(f"Posisi {ticket} berhasil ditutup.")
else:
logger.error(f"Gagal menutup posisi, retcode={result.retcode if result else 'N/A'}, comment: {result.comment if result else mt5.last_error()}")
except Exception as e:
logger.error(f"Exception saat menutup posisi: {e}", exc_info=True)
return None, str(e)
+17 -18
View File
@@ -2,28 +2,27 @@
from flask import Blueprint, jsonify
from core.utils.mt5 import get_mt5_account_info, get_todays_profit
from core.db.queries import get_db_connection
from core.db import queries # <-- 1. Impor modul queries
api_dashboard = Blueprint('api_dashboard', __name__)
@api_dashboard.route('/api/dashboard/stats')
def api_dashboard_stats():
account_info = get_mt5_account_info()
todays_profit = get_todays_profit()
try:
account_info = get_mt5_account_info()
todays_profit = get_todays_profit()
conn = get_db_connection()
active_bots_count = conn.execute("SELECT COUNT(id) FROM bots WHERE status = 'Aktif'").fetchone()[0]
active_bots_data = conn.execute("SELECT name, market FROM bots WHERE status = 'Aktif'").fetchall()
conn.close()
# 2. Ambil semua bot sekali saja untuk efisiensi
all_bots = queries.get_all_bots()
active_bots = [bot for bot in all_bots if bot['status'] == 'Aktif']
active_bots_list = [{'name': bot['name'], 'market': bot['market']} for bot in active_bots_data]
stats = {
"balance": account_info.get('balance', 0) if account_info else 0,
"equity": account_info.get('equity', 0) if account_info else 0,
"profit": account_info.get('profit', 0) if account_info else 0,
"active_bots_count": active_bots_count,
"todays_profit": todays_profit,
"active_bots": active_bots_list
}
return jsonify(stats)
stats = {
"equity": account_info.get('equity', 0) if account_info else 0,
"todays_profit": todays_profit,
"active_bots_count": len(active_bots),
"total_bots": len(all_bots), # <-- 3. Tambahkan total bot ke respon
"active_bots": [{'name': bot['name'], 'market': bot['market']} for bot in active_bots]
}
return jsonify(stats)
except Exception as e:
return jsonify({"error": f"Gagal mengambil statistik dashboard: {e}"}), 500
+22 -32
View File
@@ -1,43 +1,33 @@
# core/routes/api_notifications.py
from flask import Blueprint, jsonify, request
import sqlite3
from core.db import queries
from flask import Blueprint, jsonify
api_notifications = Blueprint('api_notifications', __name__)
def get_db():
conn = sqlite3.connect('bots.db')
conn.row_factory = sqlite3.Row
return conn
@api_notifications.route('/api/notifications')
def get_notifications():
conn = get_db()
cursor = conn.cursor()
cursor.execute('''
SELECT n.id, n.message, n.is_read, n.timestamp, b.name as bot_name
FROM notifications n
LEFT JOIN bots b ON n.bot_id = b.id
ORDER BY n.timestamp DESC
''')
notifications = [dict(row) for row in cursor.fetchall()]
conn.close()
return jsonify(notifications)
@api_notifications.route('/api/notifications', methods=['GET'])
def get_notifications_route():
"""Mengembalikan daftar notifikasi penting."""
try:
notifications = queries.get_notifications()
return jsonify(notifications)
except Exception as e:
return jsonify({"error": f"Gagal mengambil notifikasi: {e}"}), 500
@api_notifications.route('/api/notifications/unread-count')
def get_unread_notifications_count():
conn = get_db()
cursor = conn.cursor()
cursor.execute("SELECT COUNT(id) as unread_count FROM notifications WHERE is_read = 0")
count = dict(cursor.fetchone())
conn.close()
return jsonify(count)
"""Mengembalikan jumlah notifikasi yang belum dibaca."""
try:
count = queries.get_unread_notifications_count()
return jsonify(count)
except Exception as e:
return jsonify({"error": f"Gagal menghitung notifikasi: {e}"}), 500
@api_notifications.route('/api/notifications/mark-as-read', methods=['POST'])
def mark_notifications_as_read():
conn = get_db()
cursor = conn.cursor()
cursor.execute("UPDATE notifications SET is_read = 1 WHERE is_read = 0")
conn.commit()
conn.close()
return jsonify({'message': 'Semua notifikasi ditandai sudah dibaca.'})
"""Menandai semua notifikasi sebagai sudah dibaca."""
try:
queries.mark_notifications_as_read()
return jsonify({'message': 'Semua notifikasi ditandai sudah dibaca.'})
except Exception as e:
return jsonify({"error": f"Gagal menandai notifikasi: {e}"}), 500
+5 -3
View File
@@ -43,9 +43,11 @@ class BollingerSqueezeStrategy(BaseStrategy):
rsi_period = self.params.get('rsi_period', 14)
volume_factor = self.params.get('volume_factor', 1.5)
bbu_col = f'BBU_{bb_length}_{bb_std}'
bbm_col = f'BBM_{bb_length}_{bb_std}'
bbl_col = f'BBL_{bb_length}_{bb_std}'
# PERBAIKAN: Paksa format float dengan satu desimal pada nama kolom
# untuk mencegah KeyError (misal: 'BBU_20_2' vs 'BBU_20_2.0')
bbu_col = f'BBU_{bb_length}_{bb_std:.1f}'
bbm_col = f'BBM_{bb_length}_{bb_std:.1f}'
bbl_col = f'BBL_{bb_length}_{bb_std:.1f}'
df.ta.bbands(length=bb_length, std=bb_std, append=True)
df['BB_WIDTH'] = df[bbu_col] - df[bbl_col]
+100
View File
@@ -0,0 +1,100 @@
# d:\dev\quantumbotx\core\strategies\quantum_velocity.py
import pandas_ta as ta
import numpy as np
import MetaTrader5 as mt5
from .base_strategy import BaseStrategy
from core.data.fetch import get_rates
class QuantumVelocityStrategy(BaseStrategy):
name = 'Quantum Velocity'
description = 'Menggabungkan filter tren jangka panjang (EMA 200) dengan pemicu volatilitas (Bollinger Squeeze Breakout).'
@classmethod
def get_definable_params(cls):
"""Mengembalikan parameter yang bisa diatur untuk strategi ini."""
return [
{"name": "ema_period", "label": "Periode EMA Tren", "type": "number", "default": 200},
{"name": "bb_length", "label": "Panjang BB", "type": "number", "default": 20},
{"name": "bb_std", "label": "Std Dev BB", "type": "number", "default": 2.0, "step": 0.1},
{"name": "squeeze_window", "label": "Window Squeeze", "type": "number", "default": 10},
{"name": "squeeze_factor", "label": "Faktor Squeeze", "type": "number", "default": 0.7, "step": 0.1},
]
def analyze(self):
"""
Menganalisis pasar dengan filter tren jangka panjang dan pemicu breakout.
"""
tf_const = self.bot.tf_map.get(self.bot.timeframe, mt5.TIMEFRAME_H1)
# Butuh data yang cukup untuk EMA 200
df = get_rates(self.bot.market_for_mt5, tf_const, 250)
if df is None or df.empty or len(df) < 201:
return {"signal": "HOLD", "price": None, "explanation": "Data tidak cukup untuk Quantum Velocity."}
# --- Ambil Parameter ---
ema_period = self.params.get('ema_period', 200)
bb_length = self.params.get('bb_length', 20)
bb_std = self.params.get('bb_std', 2.0)
squeeze_window = self.params.get('squeeze_window', 10)
squeeze_factor = self.params.get('squeeze_factor', 0.7)
# --- Hitung Indikator ---
df[f'EMA_{ema_period}'] = ta.ema(df['close'], length=ema_period)
bbu_col = f'BBU_{bb_length}_{bb_std:.1f}'
bbm_col = f'BBM_{bb_length}_{bb_std:.1f}'
bbl_col = f'BBL_{bb_length}_{bb_std:.1f}'
df.ta.bbands(length=bb_length, std=bb_std, append=True)
df['BB_BANDWIDTH'] = np.where(df[bbm_col] != 0, (df[bbu_col] - df[bbl_col]) / df[bbm_col] * 100, 0)
df['AVG_BANDWIDTH'] = df['BB_BANDWIDTH'].rolling(window=squeeze_window).mean()
df['SQUEEZE_LEVEL'] = df['AVG_BANDWIDTH'] * squeeze_factor
df['SQUEEZE'] = df['BB_BANDWIDTH'] < df['SQUEEZE_LEVEL']
df.dropna(inplace=True)
if len(df) < 2:
return {"signal": "HOLD", "price": None, "explanation": "Indikator belum matang."}
last = df.iloc[-1]
prev = df.iloc[-2]
price = last["close"]
signal = "HOLD"
explanation = "Tidak ada kondisi yang terpenuhi."
trend_regime = "N/A"
# --- FILTER 1: Tentukan Rezim Tren (EMA 200) ---
if price > last[f'EMA_{ema_period}']:
trend_regime = "Bullish"
elif price < last[f'EMA_{ema_period}']:
trend_regime = "Bearish"
else:
trend_regime = "Choppy"
# --- FILTER 2: Cari Sinyal Pemicu (Squeeze Breakout) ---
is_in_squeeze = prev['SQUEEZE']
if is_in_squeeze:
explanation = f"Rezim: {trend_regime}. Squeeze terdeteksi, menunggu breakout."
# Hanya cari sinyal BUY jika dalam Rezim Bullish
if trend_regime == "Bullish" and last['close'] > prev[bbu_col]:
signal = "BUY"
explanation = f"Rezim: {trend_regime}. Sinyal: Squeeze & Breakout NAIK!"
# Hanya cari sinyal SELL jika dalam Rezim Bearish
elif trend_regime == "Bearish" and last['close'] < prev[bbl_col]:
signal = "SELL"
explanation = f"Rezim: {trend_regime}. Sinyal: Squeeze & Breakout TURUN!"
analysis_data = {
"signal": signal,
"price": price,
"explanation": explanation,
"Trend_Regime": trend_regime,
f"EMA_{ema_period}": last.get(f'EMA_{ema_period}'),
"Is_Squeeze": bool(last.get('SQUEEZE', False)),
}
return analysis_data
+2
View File
@@ -7,6 +7,7 @@ from .rsi_breakout import RSIBreakoutStrategy
from .bollinger_bands import BollingerBandsStrategy
from .bollinger_squeeze import BollingerSqueezeStrategy
from .mercy_edge import MercyEdgeStrategy
from .quantum_velocity import QuantumVelocityStrategy
from .pulse_sync import PulseSyncStrategy
STRATEGY_MAP = {
@@ -17,6 +18,7 @@ STRATEGY_MAP = {
'BOLLINGER_BANDS': BollingerBandsStrategy,
'BOLLINGER_SQUEEZE': BollingerSqueezeStrategy,
'MERCY_EDGE': MercyEdgeStrategy,
'quantum_velocity': QuantumVelocityStrategy, # <-- Tambahkan strategi baru
'PULSE_SYNC': PulseSyncStrategy,
}
+118
View File
@@ -0,0 +1,118 @@
# lab/backtester_hybrid_pro.py
import pandas as pd
import pandas_ta as ta
import matplotlib.pyplot as plt
from pathlib import Path
def get_profit_multiplier(symbol, lot_size=0.01):
if "500" in symbol or "100" in symbol or "30" in symbol: return 1 * lot_size
elif "XAU" in symbol: return 100 * lot_size
elif "JPY" in symbol: return 1000 * lot_size
else: return 100000 * lot_size
def run_hybrid_pro_backtest(data_path, symbol, strategy_params, initial_balance=10000):
"""
Menjalankan backtest untuk strategi QuantumBotX Hybrid versi Pro
dengan parameter yang dapat dikonfigurasi.
"""
print(f"Memulai backtest HYBRID PRO untuk simbol: {symbol}")
print(f"Parameter yang digunakan: {strategy_params}")
LOT_SIZE = 0.01
multiplier = get_profit_multiplier(symbol, LOT_SIZE)
df = pd.read_csv(data_path, parse_dates=['time'])
# --- Ambil Parameter Dinamis dari Argumen Fungsi ---
adx_period = strategy_params.get('adx_period', 14)
adx_threshold = strategy_params.get('adx_threshold', 25)
ma_fast_period = strategy_params.get('ma_fast_period', 20)
ma_slow_period = strategy_params.get('ma_slow_period', 50)
bb_length = strategy_params.get('bb_length', 20)
bb_std = strategy_params.get('bb_std', 2.0)
# --- Hitung Indikator ---
df.ta.adx(length=adx_period, append=True)
df[f'SMA_{ma_fast_period}'] = ta.sma(df['close'], length=ma_fast_period)
df[f'SMA_{ma_slow_period}'] = ta.sma(df['close'], length=ma_slow_period)
df.ta.bbands(length=bb_length, std=bb_std, append=True)
df.dropna(inplace=True)
df = df.reset_index(drop=True)
# --- Siapkan Nama Kolom Dinamis ---
adx_col = f'ADX_{adx_period}'
ma_fast_col = f'SMA_{ma_fast_period}'
ma_slow_col = f'SMA_{ma_slow_period}'
bbu_col = f'BBU_{bb_length}_{bb_std:.1f}'
bbl_col = f'BBL_{bb_length}_{bb_std:.1f}'
# --- Siapkan Variabel Simulasi ---
balance, position, trades, equity_curve = initial_balance, None, [], []
print("Memulai Loop Backtest...")
for i in range(1, len(df)):
current, prev = df.iloc[i], df.iloc[i-1]
adx_value = current[adx_col]
# --- Logika Adaptif ---
if adx_value > adx_threshold: # Mode Trending
if position and position['strategy'] == 'Bollinger': # Keluar dari posisi Bollinger jika tren dimulai
profit = (current['close'] - position['entry_price']) * multiplier if position['type'] == 'BUY' else (position['entry_price'] - current['close']) * multiplier
balance += profit; trades.append({'profit': profit}); position = None
if not position: # Entry MA Crossover
if prev[ma_fast_col] <= prev[ma_slow_col] and current[ma_fast_col] > current[ma_slow_col]:
position = {'type': 'BUY', 'entry_price': current['close'], 'strategy': 'MA_Crossover'}
elif prev[ma_fast_col] >= prev[ma_slow_col] and current[ma_fast_col] < current[ma_slow_col]:
position = {'type': 'SELL', 'entry_price': current['close'], 'strategy': 'MA_Crossover'}
elif adx_value < adx_threshold: # Mode Ranging
if position and position['strategy'] == 'MA_Crossover': # Keluar dari posisi MA jika pasar sideways
profit = (current['close'] - position['entry_price']) * multiplier if position['type'] == 'BUY' else (position['entry_price'] - current['close']) * multiplier
balance += profit; trades.append({'profit': profit}); position = None
if not position: # Entry Bollinger Bands
if current['low'] <= current[bbl_col]:
position = {'type': 'BUY', 'entry_price': current['close'], 'strategy': 'Bollinger'}
elif current['high'] >= current[bbu_col]:
position = {'type': 'SELL', 'entry_price': current['close'], 'strategy': 'Bollinger'}
equity_curve.append(balance)
# --- Analisis Hasil ---
print("\n--- Backtest Selesai ---")
print(f"Balance Awal: ${initial_balance:.2f}")
print(f"Balance Akhir: ${balance:.2f}")
print(f"Total Profit/Loss: ${balance - initial_balance:.2f} ({(balance - initial_balance)/initial_balance*100:.2f}%)")
print(f"Total Trades: {len(trades)}")
plt.figure(figsize=(12, 6))
plt.plot(df['time'].iloc[1:], equity_curve)
plt.title(f'Equity Curve - HYBRID PRO on {symbol}')
plt.xlabel('Tanggal')
plt.ylabel('Balance ($)')
plt.grid(True)
plt.show()
if __name__ == '__main__':
# --- PUSAT KONTROL EKSPERIMEN ---
# Definisikan parameter yang ingin diuji.
# Ini sama seperti `get_definable_params` di strategi Anda.
params_to_test = {
"adx_period": 14,
"adx_threshold": 25,
"ma_fast_period": 20,
"ma_slow_period": 50,
"bb_length": 20,
"bb_std": 2.0
}
# Pilih pasar yang ingin diuji
symbol_to_test = "EURUSD"
data_folder = Path(__file__).parent.resolve() # Otomatis mencari di folder 'lab/'
file_name = data_folder / f"{symbol_to_test}_16385_data.csv"
# Jalankan backtest dengan parameter dan pasar yang dipilih
run_hybrid_pro_backtest(str(file_name), symbol=symbol_to_test, strategy_params=params_to_test)
+13 -35
View File
@@ -45,12 +45,12 @@ async function fetchAllBots() {
if (activeBots.length === 0) {
listEl.innerHTML = '<p class="p-4 text-gray-500">Tidak ada bot yang sedang aktif.</p>';
} else {
activeBots.forEach(bot => {
const botsHtml = activeBots.map(bot => {
const badge = bot.strategy === 'MERCY_EDGE'
? `<span class="ml-2 px-2 py-0.5 text-xs bg-purple-100 text-purple-800 rounded-full">AI</span>`
: '';
const html = `
return `
<div class="p-4 hover:bg-gray-50 transition">
<div class="flex items-center justify-between">
<div class="flex items-center">
@@ -72,8 +72,8 @@ async function fetchAllBots() {
</div>
</div>
`;
listEl.innerHTML += html;
});
}).join('');
listEl.innerHTML = botsHtml;
}
} catch (error) {
@@ -119,7 +119,8 @@ async function updatePriceChart(symbol = 'EURUSD') {
priceChart.data.datasets[0].data = chartData.data;
priceChart.update();
} else {
priceChart = new Chart(ctx, config);
priceChart = new Chart(ctx, config); // eslint-disable-line no-undef
}
} catch (error) {
@@ -166,49 +167,26 @@ async function updateRsiChart(symbol = 'EURUSD') {
rsiChart.data.labels = rsiData.timestamps;
rsiChart.data.datasets[0].data = rsiData.rsi_values;
rsiChart.update();
} else {
rsiChart = new Chart(ctx, config);
} else {
rsiChart = new Chart(ctx, config); // eslint-disable-line no-undef
}
} catch (error) {
console.error('[AI] Gagal memuat sinyal AI:', error);
if (aiSignalSymbolEl) aiSignalSymbolEl.textContent = 'Error';
if (aiSignalDecisionEl) aiSignalDecisionEl.textContent = 'Error';
if (aiSignalExplanationEl) aiSignalExplanationEl.textContent = 'Error loading AI signal.';
if (strategyNameEl) strategyNameEl.textContent = 'Error';
console.error('[RSI Chart] Gagal update grafik RSI:', error);
const ctx = document.getElementById('rsiChart');
if (ctx && ctx.parentElement) {
ctx.parentElement.innerHTML = '<p class="text-center text-red-500">Gagal memuat data RSI.</p>';
}
}
}
// DOM ready
document.addEventListener('DOMContentLoaded', () => {
// --- Elemen Global AI Signal ---
const aiSignalSymbolEl = document.getElementById('ai-signal-symbol');
const aiSignalDecisionEl = document.getElementById('ai-signal-decision');
const aiSignalExplanationEl = document.getElementById('ai-signal-explanation');
const refreshAiSignalButton = document.getElementById('refresh-ai-signal-button');
updateDashboardStats();
fetchAllBots();
updatePriceChart();
updateRsiChart();
// Event listener untuk tombol refresh AI Signal
let isAiSignalLoading = false;
if (refreshAiSignalButton) {
refreshAiSignalButton.addEventListener('click', async () => {
if (isAiSignalLoading) return;
isAiSignalLoading = true;
refreshAiSignalButton.disabled = true;
refreshAiSignalButton.textContent = 'Refreshing...';
await fetchAiSignal();
isAiSignalLoading = false;
refreshAiSignalButton.disabled = false;
refreshAiSignalButton.textContent = 'Refresh AI Signal';
});
}
// Interval refresh
setInterval(updateDashboardStats, 10000);
setInterval(fetchAllBots, 5000);
+17 -28
View File
@@ -9,26 +9,14 @@ document.addEventListener('DOMContentLoaded', function() {
});
};
const getNotificationAppearance = (message) => {
if (message.toLowerCase().includes('berhasil') || message.toLowerCase().includes('dibuka')) {
return {
icon: 'fa-check-circle',
color: 'green'
};
}
if (message.toLowerCase().includes('gagal')) {
return {
icon: 'fa-exclamation-triangle',
color: 'red'
};
}
if (message.toLowerCase().includes('ditutup')) {
return {
icon: 'fa-flag-checkered',
color: 'blue'
};
}
return { icon: 'fa-info-circle', color: 'gray' }; // Default
const getNotificationAppearance = (action) => {
const lowerAction = action.toLowerCase();
if (lowerAction.includes('buy')) return { icon: 'fa-arrow-up', color: 'green' };
if (lowerAction.includes('sell')) return { icon: 'fa-arrow-down', color: 'red' };
if (lowerAction.includes('start')) return { icon: 'fa-play-circle', color: 'blue' };
if (lowerAction.includes('stop')) return { icon: 'fa-pause-circle', color: 'yellow' };
if (lowerAction.includes('error')) return { icon: 'fa-exclamation-triangle', color: 'red' };
return { icon: 'fa-info-circle', color: 'gray' };
};
async function fetchNotifications() {
@@ -37,25 +25,26 @@ document.addEventListener('DOMContentLoaded', function() {
if (!response.ok) throw new Error('Gagal memuat notifikasi');
const notifications = await response.json();
container.innerHTML = ''; // Kosongkan pesan "Memuat..."
if (notifications.length === 0) {
container.innerHTML = '<p class="p-6 text-center text-gray-500">Tidak ada notifikasi.</p>';
return;
}
notifications.forEach(notif => {
const appearance = getNotificationAppearance(notif.message);
const notifElement = `
const notificationsHtml = notifications.map(notif => {
const appearance = getNotificationAppearance(notif.action);
const message = `<strong>[${notif.bot_name}]</strong> ${notif.details}`;
return `
<div class="p-4 flex items-start hover:bg-gray-50 cursor-pointer">
<div class="w-10 h-10 rounded-full bg-${appearance.color}-100 flex items-center justify-center text-${appearance.color}-600 mr-4 flex-shrink-0"><i class="fas ${appearance.icon}"></i></div>
<div>
<p class="text-sm text-gray-800">${notif.message}</p>
<p class="text-sm text-gray-800">${message}</p>
<p class="text-xs text-gray-500 mt-1">${formatTimestamp(notif.timestamp)}</p>
</div>
</div>`;
container.innerHTML += notifElement;
});
}).join('');
container.innerHTML = notificationsHtml;
} catch (error) {
console.error("Error fetching notifications:", error);
container.innerHTML = `<p class="p-6 text-center text-red-500">Gagal memuat notifikasi: ${error.message}</p>`;
+3
View File
@@ -6,6 +6,7 @@ document.addEventListener('DOMContentLoaded', function() {
const modal = document.getElementById('create-bot-modal');
const form = document.getElementById('create-bot-form');
const modalTitle = document.getElementById('modal-title');
const submitBtn = document.getElementById('submit-bot-btn'); // <-- 1. Ambil elemen tombol
const createBotBtn = document.getElementById('create-bot-btn');
const cancelBtn = document.getElementById('cancel-create');
const paramsContainer = document.getElementById('strategy-params-container');
@@ -108,6 +109,7 @@ document.addEventListener('DOMContentLoaded', function() {
currentBotId = null;
paramsContainer.innerHTML = ''; // Kosongkan parameter
form.reset();
submitBtn.textContent = 'Buat Bot'; // <-- 2. Set teks untuk mode 'Create'
modalTitle.textContent = '🚀 Buat Bot Baru';
// Set nilai default
form.elements.lot_size.value = 0.01;
@@ -183,6 +185,7 @@ document.addEventListener('DOMContentLoaded', function() {
const bot = await res.json();
if (res.ok) {
currentBotId = botId;
submitBtn.textContent = 'Ubah Bot'; // <-- 3. Set teks untuk mode 'Edit'
modalTitle.textContent = '✏️ Edit Bot';
// Isi form dengan data bot yang ada
for (const key in bot) {
+4 -17
View File
@@ -78,19 +78,19 @@
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4 mb-6">
<div class="bg-white rounded-lg shadow p-6">
<h3 class="text-sm font-medium text-gray-500">Total Saldo (Equity)</h3>
<p id="total-equity" class="mt-1 text-3xl font-semibold text-gray-800">$0.00</p>
<p id="total-equity" class="mt-1 text-3xl font-semibold text-gray-800"><i class="fas fa-spinner fa-spin text-gray-400"></i></p>
</div>
<div class="bg-white rounded-lg shadow p-6">
<h3 class="text-sm font-medium text-gray-500">Profit Hari Ini</h3>
<p id="todays-profit" class="mt-1 text-3xl font-semibold text-green-500">$0.00</p>
<p id="todays-profit" class="mt-1 text-3xl font-semibold text-gray-800"><i class="fas fa-spinner fa-spin text-gray-400"></i></p>
</div>
<div class="bg-white rounded-lg shadow p-6">
<h3 class="text-sm font-medium text-gray-500">Bot Aktif</h3>
<p id="active-bots-count" class="mt-1 text-3xl font-semibold text-gray-800">0</p>
<p id="active-bots-count" class="mt-1 text-3xl font-semibold text-gray-800"><i class="fas fa-spinner fa-spin text-gray-400"></i></p>
</div>
<div class="bg-white rounded-lg shadow p-6">
<h3 class="text-sm font-medium text-gray-500">Total Bot</h3>
<p id="total-bots-count" class="mt-1 text-3xl font-semibold text-gray-800">0</p>
<p id="total-bots-count" class="mt-1 text-3xl font-semibold text-gray-800"><i class="fas fa-spinner fa-spin text-gray-400"></i></p>
</div>
</div>
@@ -105,19 +105,6 @@
<div class="chart-container"><canvas id="rsiChart" height="100"></canvas></div>
</div>
<!-- AI SIGNAL -->
<div class="bg-white rounded-lg shadow p-6 mb-6">
<h3 class="text-md font-semibold text-gray-700 mb-2 flex items-center">
💡 AI Signal <span id="ai-strategy-name" class="ml-2 px-2 py-0.5 text-xs bg-purple-100 text-purple-800 rounded-full">Loading Strategy...</span>
</h3>
<button id="refresh-ai-signal-button" class="mb-2 px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50">Refresh AI Signal</button>
<div id="ai-signal-container" class="text-sm text-gray-800 space-y-2">
<p><strong>Symbol:</strong> <span id="ai-signal-symbol">N/A</span></p>
<p><strong>Decision:</strong> <span id="ai-signal-decision">N/A</span></p>
<p class="italic text-gray-600" id="ai-signal-explanation">Click the button above to load AI signal.</p>
</div>
</div>
<!-- BOT AKTIF -->
<div class="bg-white rounded-lg shadow">
<div class="p-6 border-b">
+2 -2
View File
@@ -162,12 +162,12 @@
<!-- Footer Modal (Tidak akan ikut ter-scroll) -->
<div class="flex-shrink-0 flex items-center p-6 space-x-2 border-t border-gray-200 rounded-b">
<button type="submit" form="create-bot-form" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center">Buat Bot</button>
<button type="submit" id="submit-bot-btn" form="create-bot-form" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center">Buat Bot</button>
<button type="button" id="cancel-create-footer" class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10">Batal</button>
</div>
</div>
</div>
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
<script src="{{ url_for('static', filename='js/trading_bots.js') }}"></script>
</body>
</html>