mirror of
https://github.com/chrisnov-it/quantumbotx.git
synced 2026-07-28 03:07:53 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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>`;
|
||||
|
||||
@@ -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
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user