From ce22b46b487a0ad468c0d9911ec2c0b9e6c294ad Mon Sep 17 00:00:00 2001 From: Reynov Christian Date: Fri, 1 Aug 2025 01:16:39 +0800 Subject: [PATCH] feat: Implement Hybrid Strategy & Major Bug Fixes Successfully run QuantumBotX Hybrid on XAUUSD after fixing critical bugs (KeyError, data fetching). Improved UI with scrollable modal and implemented graceful shutdown for the server. System is now more robust and user-friendly. --- app.py | 21 +++++ core/bots/controller.py | 24 ++++- core/bots/trading_bot.py | 13 ++- core/db/connection.py | 27 ++---- core/db/queries.py | 14 +-- core/routes/api_bots.py | 23 ++++- core/routes/api_fundamentals.py | 13 +-- core/strategies/base_strategy.py | 14 ++- core/strategies/bollinger_bands.py | 31 +++++-- core/strategies/bollinger_squeeze.py | 57 ++++++++---- core/strategies/ma_crossover.py | 22 ++++- core/strategies/mercy_edge.py | 65 ++++++++----- core/strategies/quantumbotx_hybrid.py | 69 ++++++++++---- core/strategies/rsi_breakout.py | 53 +++++++---- static/js/bot_detail.js | 19 +++- static/js/trading_bots.js | 59 ++++++++++++ templates/trading_bots.html | 129 ++++++++++++++++---------- 17 files changed, 466 insertions(+), 187 deletions(-) diff --git a/app.py b/app.py index b8c649e..aae3c20 100644 --- a/app.py +++ b/app.py @@ -2,10 +2,12 @@ import os import logging from logging.handlers import RotatingFileHandler +import atexit # <-- 1. Impor modul atexit from flask import Flask, render_template, send_from_directory from dotenv import load_dotenv # Import modul inti yang diperlukan saat startup +import MetaTrader5 as mt5 # <-- 2. Impor MT5 secara langsung from core.utils.mt5 import initialize_mt5 from core.bots.controller import ambil_semua_bot @@ -116,6 +118,22 @@ def favicon(): return send_from_directory(os.path.join(app.root_path, 'static'), 'favicon.ico', mimetype='image/vnd.microsoft.icon') +# --- Fungsi Shutdown --- +def shutdown_handler(): + """Fungsi yang akan dipanggil saat aplikasi akan keluar.""" + logger.info("Menerima sinyal shutdown. Memulai proses pembersihan...") + + # Impor controller di sini untuk menghindari circular import + from core.bots import controller + + # 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) + + mt5.shutdown() + logger.info("Koneksi MetaTrader 5 ditutup. Shutdown selesai.") + # --- Titik Eksekusi Utama --- if __name__ == '__main__': # Memuat kredensial MT5 dari .env dengan aman @@ -146,6 +164,9 @@ if __name__ == '__main__': except Exception as e: logger.error(f"Terjadi kesalahan saat memuat bot: {e}", exc_info=True) + # --- 3. Daftarkan fungsi shutdown --- + atexit.register(shutdown_handler) + # Jalankan aplikasi Flask app.run( debug=os.getenv('FLASK_DEBUG', 'False').lower() == 'true', diff --git a/core/bots/controller.py b/core/bots/controller.py index dc252d4..2e2aa16 100644 --- a/core/bots/controller.py +++ b/core/bots/controller.py @@ -1,5 +1,6 @@ # core/bots/controller.py +import json import logging from core.db import queries from .trading_bot import TradingBot @@ -38,12 +39,16 @@ def mulai_bot(bot_id: int): if not bot_data: return False, f"Bot dengan ID {bot_id} tidak ditemukan." + # Ubah string JSON dari DB menjadi dictionary Python + params_dict = json.loads(bot_data.get('strategy_params', '{}')) + try: bot_thread = TradingBot( id=bot_data['id'], name=bot_data['name'], market=bot_data['market'], lot_size=bot_data['lot_size'], sl_pips=bot_data['sl_pips'], tp_pips=bot_data['tp_pips'], timeframe=bot_data['timeframe'], - check_interval=bot_data['check_interval_seconds'], strategy=bot_data['strategy'] + check_interval=bot_data['check_interval_seconds'], strategy=bot_data['strategy'], + strategy_params=params_dict ) bot_thread.start() active_bots[bot_id] = bot_thread @@ -82,10 +87,23 @@ def perbarui_bot(bot_id: int, data: dict): # menjadi 'interval' yang sesuai dengan kolom database. if 'check_interval_seconds' in data: data['interval'] = data.pop('check_interval_seconds') - # --- AKHIR PERBAIKAN --- + + # Ambil parameter kustom, ubah jadi string JSON, dan simpan + custom_params = data.pop('params', {}) + data['strategy_params'] = json.dumps(custom_params) + + # --- PERBAIKAN BARU: Filter data untuk mencegah TypeError --- + # Hanya teruskan argumen yang diharapkan oleh fungsi queries.update_bot + expected_args = [ + 'name', 'market', 'lot_size', 'sl_pips', 'tp_pips', + 'timeframe', 'interval', 'strategy', 'strategy_params' + ] + + update_data = {key: data[key] for key in expected_args if key in data} try: - success = queries.update_bot(bot_id=bot_id, **data) + # Gunakan dictionary yang sudah difilter + success = queries.update_bot(bot_id=bot_id, **update_data) if success: logger.info(f"Konfigurasi bot {bot_id} berhasil diperbarui di database.") return True, "Bot berhasil diperbarui." diff --git a/core/bots/trading_bot.py b/core/bots/trading_bot.py index 1fb3607..b029510 100644 --- a/core/bots/trading_bot.py +++ b/core/bots/trading_bot.py @@ -12,7 +12,7 @@ logger = logging.getLogger(__name__) class TradingBot(threading.Thread): - def __init__(self, id, name, market, lot_size, sl_pips, tp_pips, timeframe, check_interval, strategy, status='Dijeda'): + 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 @@ -23,6 +23,7 @@ class TradingBot(threading.Thread): 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 @@ -43,7 +44,7 @@ class TradingBot(threading.Thread): raise ValueError(f"Strategi '{self.strategy_name}' tidak ditemukan.") # --- PERBAIKAN: Inisialisasi kelas strategi dengan benar --- - self.strategy_instance = strategy_class(bot_instance=self) + self.strategy_instance = strategy_class(bot_instance=self, params=self.strategy_params) except Exception as e: self.log_activity('ERROR', f"Inisialisasi Gagal: {e}") @@ -53,13 +54,17 @@ class TradingBot(threading.Thread): while not self._stop_event.is_set(): try: if not mt5.symbol_select(self.market_for_mt5, True): - self.log_activity('WARNING', f"Gagal mengaktifkan simbol {self.market} (di MT5: {self.market_for_mt5}). Pastikan simbol ada di Market Watch.") + 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: - self.log_activity('WARNING', f"Tidak dapat mengambil info untuk simbol {self.market} (di MT5: {self.market_for_mt5}).") + 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 diff --git a/core/db/connection.py b/core/db/connection.py index b5d7af3..2e1fa6d 100644 --- a/core/db/connection.py +++ b/core/db/connection.py @@ -1,24 +1,15 @@ -# core/db/connection.py - VERSI FINAL - +# core/db/connection.py import sqlite3 import os -# Menentukan path absolut ke file database -# Ini memastikan DB ditemukan dari mana pun skrip dijalankan +# Tentukan nama file database di satu tempat. +DATABASE_FILENAME = 'bots.db' BASE_DIR = os.path.dirname(os.path.abspath(__file__)) -DB_PATH = os.path.join(BASE_DIR, '..', '..', 'bots.db') +DATABASE_PATH = os.path.join(BASE_DIR, '..', '..', DATABASE_FILENAME) def get_db_connection(): - """ - Membuat dan mengembalikan koneksi ke database SQLite. - Fungsi ini adalah satu-satunya sumber koneksi database untuk seluruh aplikasi. - """ - try: - # check_same_thread=False diperlukan untuk aplikasi multi-threaded seperti Flask - conn = sqlite3.connect(DB_PATH, check_same_thread=False) - # 'row_factory' membuat hasil query bisa diakses seperti dictionary - conn.row_factory = sqlite3.Row - return conn - except sqlite3.Error as e: - print(f"FATAL: Gagal koneksi ke database di {DB_PATH}: {e}") - return None + """Membuat dan mengembalikan koneksi ke database SQLite.""" + conn = sqlite3.connect(DATABASE_PATH) + # Mengatur agar hasil query bisa diakses seperti dictionary + conn.row_factory = sqlite3.Row + return conn \ No newline at end of file diff --git a/core/db/queries.py b/core/db/queries.py index 76f4a39..7edf65c 100644 --- a/core/db/queries.py +++ b/core/db/queries.py @@ -26,31 +26,31 @@ def get_bot_by_id(bot_id): logger.error(f"Database error saat mengambil bot {bot_id}: {e}") return None -def add_bot(name, market, lot_size, sl_pips, tp_pips, timeframe, interval, strategy): +def add_bot(name, market, lot_size, sl_pips, tp_pips, timeframe, interval, strategy, strategy_params='{}'): """Menambahkan bot baru ke database.""" try: with get_db_connection() as conn: cursor = conn.cursor() cursor.execute(''' - INSERT INTO bots (name, market, lot_size, sl_pips, tp_pips, timeframe, check_interval_seconds, strategy, status) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'Dijeda') - ''', (name, market, lot_size, sl_pips, tp_pips, timeframe, interval, strategy)) + INSERT INTO bots (name, market, lot_size, sl_pips, tp_pips, timeframe, check_interval_seconds, strategy, strategy_params, status) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'Dijeda') + ''', (name, market, lot_size, sl_pips, tp_pips, timeframe, interval, strategy, strategy_params)) conn.commit() return cursor.lastrowid except sqlite3.Error as e: logger.error(f"Gagal menambah bot ke DB: {e}", exc_info=True) return None -def update_bot(bot_id, name, market, lot_size, sl_pips, tp_pips, timeframe, interval, strategy): +def update_bot(bot_id, name, market, lot_size, sl_pips, tp_pips, timeframe, interval, strategy, strategy_params='{}'): """Memperbarui data bot yang sudah ada di database.""" try: with get_db_connection() as conn: conn.execute(''' UPDATE bots SET name = ?, market = ?, lot_size = ?, sl_pips = ?, tp_pips = ?, - timeframe = ?, check_interval_seconds = ?, strategy = ? + timeframe = ?, check_interval_seconds = ?, strategy = ?, strategy_params = ? WHERE id = ? - ''', (name, market, lot_size, sl_pips, tp_pips, timeframe, interval, strategy, bot_id)) + ''', (name, market, lot_size, sl_pips, tp_pips, timeframe, interval, strategy, strategy_params, bot_id)) conn.commit() return True except sqlite3.Error as e: diff --git a/core/routes/api_bots.py b/core/routes/api_bots.py index a412483..31f5d5a 100644 --- a/core/routes/api_bots.py +++ b/core/routes/api_bots.py @@ -1,5 +1,6 @@ # core/routes/api_bots.py - VERSI PERBAIKAN LENGKAP +import json import logging from flask import Blueprint, jsonify, request import pandas_ta as ta @@ -28,6 +29,20 @@ def get_strategies_route(): logger.error(f"Gagal memuat daftar strategi: {e}", exc_info=True) return jsonify({"error": "Gagal memuat daftar strategi"}), 500 +@api_bots.route('/api/strategies//params', methods=['GET']) +def get_strategy_params_route(strategy_id): + """Mengembalikan parameter yang bisa diatur untuk sebuah strategi.""" + strategy_class = STRATEGY_MAP.get(strategy_id) + if not strategy_class: + return jsonify({"error": "Strategi tidak ditemukan"}), 404 + + # Panggil metode class untuk mendapatkan parameter + if hasattr(strategy_class, 'get_definable_params'): + params = strategy_class.get_definable_params() + return jsonify(params) + + return jsonify([]) # Kembalikan array kosong jika tidak ada parameter + @api_bots.route('/api/bots', methods=['GET']) def get_bots_route(): """Mengambil semua bot.""" @@ -46,16 +61,22 @@ def get_bots_route(): def get_single_bot_route(bot_id): """Mengambil detail satu bot.""" bot = queries.get_bot_by_id(bot_id) + if bot and bot.get('strategy_params'): + # Ubah string JSON menjadi objek untuk frontend + bot['strategy_params'] = json.loads(bot['strategy_params']) return jsonify(bot) if bot else (jsonify({"error": "Bot tidak ditemukan"}), 404) @api_bots.route('/api/bots', methods=['POST']) def add_bot_route(): """Membuat bot baru.""" data = request.get_json() + params_json = json.dumps(data.get('params', {})) + new_bot_id = queries.add_bot( name=data.get('name'), market=data.get('market'), lot_size=data.get('lot_size'), sl_pips=data.get('sl_pips'), tp_pips=data.get('tp_pips'), timeframe=data.get('timeframe'), - interval=data.get('check_interval_seconds'), strategy=data.get('strategy') + interval=data.get('check_interval_seconds'), strategy=data.get('strategy'), + strategy_params=params_json ) if new_bot_id: controller.add_new_bot_to_controller(new_bot_id) diff --git a/core/routes/api_fundamentals.py b/core/routes/api_fundamentals.py index f18666c..09416e4 100644 --- a/core/routes/api_fundamentals.py +++ b/core/routes/api_fundamentals.py @@ -1,21 +1,14 @@ # core/routes/api_fundamentals.py from flask import Blueprint, jsonify -import sqlite3 - +# Hapus 'import sqlite3' dan gunakan fungsi dari queries +from core.db import queries api_fundamentals = Blueprint('api_fundamentals', __name__) -def get_bot(bot_id): - conn = sqlite3.connect('bots.db') - conn.row_factory = sqlite3.Row - bot = conn.execute('SELECT * FROM bots WHERE id = ?', (bot_id,)).fetchone() - conn.close() - return bot - @api_fundamentals.route('/api/bots//fundamentals') def get_bot_fundamentals(bot_id): - bot = get_bot(bot_id) + bot = queries.get_bot_by_id(bot_id) # Gunakan fungsi terpusat if not bot: return jsonify({'error': 'Bot tidak ditemukan'}), 404 diff --git a/core/strategies/base_strategy.py b/core/strategies/base_strategy.py index cba84cf..a2fbfae 100644 --- a/core/strategies/base_strategy.py +++ b/core/strategies/base_strategy.py @@ -5,18 +5,28 @@ class BaseStrategy: Kelas dasar abstrak untuk semua strategi trading. Setiap strategi harus mewarisi kelas ini dan mengimplementasikan metode `analyze`. """ - def __init__(self, bot_instance): + def __init__(self, bot_instance, params: dict = {}): """ Inisialisasi strategi dengan instance dari bot yang menjalankannya. Args: bot_instance (TradingBot): Instance dari bot yang aktif. + params (dict): Dictionary berisi parameter kustom untuk strategi. """ self.bot = bot_instance + self.params = params def analyze(self): """ Metode inti yang harus di-override oleh setiap strategi turunan. Metode ini harus mengembalikan sebuah dictionary yang berisi hasil analisis. """ - raise NotImplementedError("Setiap strategi harus mengimplementasikan metode `analyze()`.") \ No newline at end of file + raise NotImplementedError("Setiap strategi harus mengimplementasikan metode `analyze()`.") + + @classmethod + def get_definable_params(cls): + """ + Metode kelas yang mengembalikan daftar parameter yang bisa diatur oleh pengguna. + Setiap strategi turunan harus meng-override ini jika memiliki parameter. + """ + return [] \ No newline at end of file diff --git a/core/strategies/bollinger_bands.py b/core/strategies/bollinger_bands.py index b71753f..ff0c987 100644 --- a/core/strategies/bollinger_bands.py +++ b/core/strategies/bollinger_bands.py @@ -7,6 +7,14 @@ class BollingerBandsStrategy(BaseStrategy): name = 'Bollinger Bands Reversion' description = 'Sinyal berdasarkan harga yang menyentuh atau melintasi batas atas atau bawah Bollinger Bands (Mean Reversion).' + @classmethod + def get_definable_params(cls): + """Mengembalikan parameter yang bisa diatur untuk strategi ini.""" + return [ + {"name": "bb_length", "label": "Panjang BB", "type": "number", "default": 20}, + {"name": "bb_std", "label": "Standar Deviasi BB", "type": "number", "default": 2.0, "step": 0.1} + ] + def analyze(self): """ Menganalisis pasar menggunakan strategi Bollinger Bandsยฎ Mean Reversion. @@ -21,7 +29,14 @@ class BollingerBandsStrategy(BaseStrategy): return {"signal": "HOLD", "price": None, "explanation": "Data tidak cukup untuk Bollinger Bands."} # --- Hitung Indikator --- - df.ta.bbands(length=20, std=2.0, append=True) + bb_length = self.params.get('bb_length', 20) + bb_std = self.params.get('bb_std', 2.0) + + # PERBAIKAN: Paksa format float dengan satu desimal pada nama kolom + bbu_col = f'BBU_{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) # pandas-ta akan membuat kolom dengan format yang sama df.dropna(inplace=True) if len(df) < 1: @@ -34,20 +49,20 @@ class BollingerBandsStrategy(BaseStrategy): # --- Logika Sinyal --- # Sinyal Beli: Harga menyentuh atau menembus Band Bawah - if last['low'] <= last['BBL_20_2.0']: + if last['low'] <= last[bbl_col]: signal = "BUY" - explanation = f"Oversold: Harga [{last['low']:.4f}] menyentuh Band Bawah [{last['BBL_20_2.0']:.4f}]" + explanation = f"Oversold: Harga [{last['low']:.4f}] menyentuh Band Bawah [{last[bbl_col]:.4f}]" # Sinyal Jual: Harga menyentuh atau menembus Band Atas - elif last['high'] >= last['BBU_20_2.0']: + elif last['high'] >= last[bbu_col]: signal = "SELL" - explanation = f"Overbought: Harga [{last['high']:.4f}] menyentuh Band Atas [{last['BBU_20_2.0']:.4f}]" + explanation = f"Overbought: Harga [{last['high']:.4f}] menyentuh Band Atas [{last[bbu_col]:.4f}]" analysis_data = { "signal": signal, "price": price, "explanation": explanation, - "BB_Upper": last.get('BBU_20_2.0'), - "BB_Middle": last.get('BBM_20_2.0'), - "BB_Lower": last.get('BBL_20_2.0'), + "BB_Upper": last.get(f'BBU_{bb_length}_{bb_std:.1f}'), + "BB_Middle": last.get(f'BBM_{bb_length}_{bb_std:.1f}'), + "BB_Lower": last.get(f'BBL_{bb_length}_{bb_std:.1f}'), } return analysis_data \ No newline at end of file diff --git a/core/strategies/bollinger_squeeze.py b/core/strategies/bollinger_squeeze.py index 0748794..c70c068 100644 --- a/core/strategies/bollinger_squeeze.py +++ b/core/strategies/bollinger_squeeze.py @@ -9,6 +9,18 @@ class BollingerSqueezeStrategy(BaseStrategy): name = 'Bollinger Squeeze Breakout' description = 'Mencari periode volatilitas rendah (squeeze) sebagai sinyal potensi breakout harga yang kuat.' + @classmethod + def get_definable_params(cls): + """Mengembalikan parameter yang bisa diatur untuk strategi ini.""" + return [ + {"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}, + {"name": "rsi_period", "label": "Periode RSI", "type": "number", "default": 14}, + {"name": "volume_factor", "label": "Faktor Volume", "type": "number", "default": 1.5, "step": 0.1} + ] + def analyze(self): """ Menganalisis pasar menggunakan strategi Bollinger Band Squeeze. @@ -24,25 +36,36 @@ class BollingerSqueezeStrategy(BaseStrategy): return {"signal": "HOLD", "price": None, "explanation": "Data tidak cukup untuk Bollinger Squeeze."} # --- Hitung Indikator --- - df.ta.bbands(length=20, std=2.0, append=True) - df['BB_WIDTH'] = df['BBU_20_2.0'] - df['BBL_20_2.0'] + 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) + 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}' + + df.ta.bbands(length=bb_length, std=bb_std, append=True) + df['BB_WIDTH'] = df[bbu_col] - df[bbl_col] # FIX 1: Mencegah ZeroDivisionError dengan pembagian yang aman df['BB_BANDWIDTH'] = np.where( - df['BBM_20_2.0'] != 0, - (df['BBU_20_2.0'] - df['BBL_20_2.0']) / df['BBM_20_2.0'] * 100, + df[bbm_col] != 0, + (df[bbu_col] - df[bbl_col]) / df[bbm_col] * 100, 0 # Jika middle band 0, anggap bandwidth 0 ) - df['AVG_BANDWIDTH'] = df['BB_BANDWIDTH'].rolling(window=10).mean() - df['SQUEEZE_LEVEL'] = df['AVG_BANDWIDTH'] * 0.7 + 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['RSI'] = ta.rsi(df['close'], length=14) + df['RSI'] = ta.rsi(df['close'], length=rsi_period) # FIX 2: Menggunakan nama kolom volume yang benar ('tick_volume') if 'tick_volume' in df.columns: - df['AVG_VOLUME'] = df['tick_volume'].rolling(window=10).mean() - df['VOLUME_SURGE'] = df['tick_volume'] > df['AVG_VOLUME'] * 1.5 + df['AVG_VOLUME'] = df['tick_volume'].rolling(window=squeeze_window).mean() + df['VOLUME_SURGE'] = df['tick_volume'] > df['AVG_VOLUME'] * volume_factor df['VOLUME_SURGE'] = df['VOLUME_SURGE'].astype(int) else: df['VOLUME_SURGE'] = 0 @@ -68,21 +91,21 @@ class BollingerSqueezeStrategy(BaseStrategy): explanation = f"Squeeze terdeteksi (Lebar: {prev['BB_WIDTH']:.4f}). Menunggu breakout." # Kondisi 2: Jika ya, apakah candle SEKARANG breakout dari Bands SEBELUMNYA? - if last['close'] > prev['BBU_20_2.0'] and last['RSI'] < 70: + if last['close'] > prev[bbu_col] and last['RSI'] < 70: signal = "BUY" - explanation = f"Squeeze & Breakout NAIK! Harga [{last['close']:.2f}] > Band Atas [{prev['BBU_20_2.0']:.2f}]" - elif last['close'] < prev['BBL_20_2.0'] and last['RSI'] > 30: + explanation = f"Squeeze & Breakout NAIK! Harga [{last['close']:.2f}] > Band Atas [{prev[bbu_col]:.2f}]" + elif last['close'] < prev[bbl_col] and last['RSI'] > 30: signal = "SELL" - explanation = f"Squeeze & Breakout TURUN! Harga [{last['close']:.2f}] < Band Bawah [{prev['BBL_20_2.0']:.2f}]" + explanation = f"Squeeze & Breakout TURUN! Harga [{last['close']:.2f}] < Band Bawah [{prev[bbl_col]:.2f}]" else: # Kondisi 3: Post-Squeeze Momentum if prev['BB_BANDWIDTH'] > prev['AVG_BANDWIDTH'] * 1.2: # Bands expanding - if last['close'] > prev['BBU_20_2.0'] and last['VOLUME_SURGE']: + if last['close'] > prev[bbu_col] and last['VOLUME_SURGE']: signal = 'BUY' - explanation = f"Momentum NAIK! Harga [{last['close']:.2f}] > Band Atas [{prev['BBU_20_2.0']:.2f}] dengan volume" - elif last['close'] < prev['BBL_20_2.0'] and last['VOLUME_SURGE']: + explanation = f"Momentum NAIK! Harga [{last['close']:.2f}] > Band Atas [{prev[bbu_col]:.2f}] dengan volume" + elif last['close'] < prev[bbl_col] and last['VOLUME_SURGE']: signal = 'SELL' - explanation = f"Momentum TURUN! Harga [{last['close']:.2f}] < Band Bawah [{prev['BBL_20_2.0']:.2f}] dengan volume" + explanation = f"Momentum TURUN! Harga [{last['close']:.2f}] < Band Bawah [{prev[bbl_col]:.2f}] dengan volume" # --- PERBAIKAN: Kembalikan semua data analisis yang relevan --- analysis_data = { diff --git a/core/strategies/ma_crossover.py b/core/strategies/ma_crossover.py index 854aeb2..f902f44 100644 --- a/core/strategies/ma_crossover.py +++ b/core/strategies/ma_crossover.py @@ -8,6 +8,14 @@ class MACrossoverStrategy(BaseStrategy): name = 'Moving Average Crossover' description = 'Sinyal berdasarkan persilangan antara dua Moving Averages (misal, 20 & 50). Cocok untuk pasar trending.' + @classmethod + def get_definable_params(cls): + """Mengembalikan parameter yang bisa diatur untuk strategi ini.""" + return [ + {"name": "fast_period", "label": "Periode MA Cepat", "type": "number", "default": 20}, + {"name": "slow_period", "label": "Periode MA Lambat", "type": "number", "default": 50} + ] + def analyze(self): """ Menganalisis pasar menggunakan strategi Moving Average Crossover (20/50). @@ -23,8 +31,12 @@ class MACrossoverStrategy(BaseStrategy): return {"signal": "HOLD", "price": None, "explanation": "Data tidak cukup untuk MA Crossover."} # --- Hitung Indikator --- - df["ma_fast"] = ta.sma(df["close"], length=20) - df["ma_slow"] = ta.sma(df["close"], length=50) + # Gunakan parameter dinamis, dengan fallback ke nilai default + fast_period = self.params.get('fast_period', 20) + slow_period = self.params.get('slow_period', 50) + + df["ma_fast"] = ta.sma(df["close"], length=fast_period) + df["ma_slow"] = ta.sma(df["close"], length=slow_period) df.dropna(inplace=True) if len(df) < 2: @@ -35,17 +47,17 @@ class MACrossoverStrategy(BaseStrategy): price = last["close"] signal = "HOLD" - explanation = f"MA(20): {last['ma_fast']:.2f}, MA(50): {last['ma_slow']:.2f}. Tidak ada sinyal." + explanation = f"MA({fast_period}): {last['ma_fast']:.2f}, MA({slow_period}): {last['ma_slow']:.2f}. Tidak ada sinyal." # --- Logika Sinyal --- # Golden Cross (Sinyal Beli) if prev["ma_fast"] <= prev["ma_slow"] and last["ma_fast"] > last["ma_slow"]: signal = "BUY" - explanation = f"Golden Cross: MA(20) [{last['ma_fast']:.2f}] memotong ke atas MA(50) [{last['ma_slow']:.2f}]" + explanation = f"Golden Cross: MA({fast_period}) [{last['ma_fast']:.2f}] memotong ke atas MA({slow_period}) [{last['ma_slow']:.2f}]" # Death Cross (Sinyal Jual) elif prev["ma_fast"] >= prev["ma_slow"] and last["ma_fast"] < last["ma_slow"]: signal = "SELL" - explanation = f"Death Cross: MA(20) [{last['ma_fast']:.2f}] memotong ke bawah MA(50) [{last['ma_slow']:.2f}]" + explanation = f"Death Cross: MA({fast_period}) [{last['ma_fast']:.2f}] memotong ke bawah MA({slow_period}) [{last['ma_slow']:.2f}]" return { "signal": signal, "price": price, "explanation": explanation, diff --git a/core/strategies/mercy_edge.py b/core/strategies/mercy_edge.py index 533c364..da77779 100644 --- a/core/strategies/mercy_edge.py +++ b/core/strategies/mercy_edge.py @@ -2,28 +2,52 @@ import MetaTrader5 as mt5 import pandas_ta as ta from .base_strategy import BaseStrategy -from core.utils.mt5 import get_rates_from_mt5 +from core.data.fetch import get_rates class MercyEdgeStrategy(BaseStrategy): name = 'Mercy Edge (AI)' description = 'Strategi hybrid yang menggabungkan MACD, Stochastic, dan validasi AI untuk sinyal presisi tinggi.' + @classmethod + def get_definable_params(cls): + """Mengembalikan parameter yang bisa diatur untuk strategi ini.""" + return [ + {"name": "macd_fast", "label": "MACD Fast", "type": "number", "default": 12}, + {"name": "macd_slow", "label": "MACD Slow", "type": "number", "default": 26}, + {"name": "macd_signal", "label": "MACD Signal", "type": "number", "default": 9}, + {"name": "stoch_k", "label": "Stoch %K", "type": "number", "default": 14}, + {"name": "stoch_d", "label": "Stoch %D", "type": "number", "default": 3}, + {"name": "stoch_smooth", "label": "Stoch Smooth", "type": "number", "default": 3}, + ] + def analyze(self): - df_d1 = get_rates_from_mt5(self.bot.market_for_mt5, mt5.TIMEFRAME_D1, 200) - df_h1 = get_rates_from_mt5(self.bot.market_for_mt5, mt5.TIMEFRAME_H1, 100) + # Ambil parameter dinamis + macd_fast = self.params.get('macd_fast', 12) + macd_slow = self.params.get('macd_slow', 26) + macd_signal_p = self.params.get('macd_signal', 9) # 'signal' adalah nama variabel yang sudah ada + stoch_k = self.params.get('stoch_k', 14) + stoch_d = self.params.get('stoch_d', 3) + stoch_smooth = self.params.get('stoch_smooth', 3) + + # Gunakan fungsi get_rates yang terstandarisasi + df_d1 = get_rates(self.bot.market_for_mt5, mt5.TIMEFRAME_D1, 200) + df_h1 = get_rates(self.bot.market_for_mt5, mt5.TIMEFRAME_H1, 100) if df_d1 is None or df_h1 is None or len(df_d1) < 50 or len(df_h1) < 30: return {"signal": "HOLD", "price": None, "explanation": "Data tidak cukup"} - macd_d1 = ta.macd(df_d1['close']).rename(columns={'MACDh_12_26_9': 'hist_d1'}) - macd_h1 = ta.macd(df_h1['close']).rename(columns={'MACDh_12_26_9': 'hist_h1'}) - stoch = ta.stoch(df_h1['high'], df_h1['low'], df_h1['close']) + # Buat nama kolom indikator secara dinamis + macd_hist_col = f'MACDh_{macd_fast}_{macd_slow}_{macd_signal_p}' + stoch_k_col = f'STOCHk_{stoch_k}_{stoch_d}_{stoch_smooth}' + stoch_d_col = f'STOCHd_{stoch_k}_{stoch_d}_{stoch_smooth}' - df_d1 = df_d1.join(macd_d1).dropna() - df_h1 = df_h1.join(macd_h1) - df_h1['stoch_k'] = stoch.iloc[:, 0] - df_h1['stoch_d'] = stoch.iloc[:, 1] - df_h1 = df_h1.dropna() + # Hitung indikator dengan parameter dinamis + df_d1.ta.macd(fast=macd_fast, slow=macd_slow, signal=macd_signal_p, append=True) + df_h1.ta.macd(fast=macd_fast, slow=macd_slow, signal=macd_signal_p, append=True) + df_h1.ta.stoch(k=stoch_k, d=stoch_d, smooth_k=stoch_smooth, append=True) + + df_d1.dropna(inplace=True) + df_h1.dropna(inplace=True) if len(df_d1) < 1 or len(df_h1) < 2: return {"signal": "HOLD", "price": None, "explanation": "Data indikator belum matang."} @@ -32,18 +56,13 @@ class MercyEdgeStrategy(BaseStrategy): last_h1 = df_h1.iloc[-1] prev_h1 = df_h1.iloc[-2] + # Gunakan nama kolom dinamis dalam logika ta_suggestion = "HOLD" - if ( - last_d1['hist_d1'] > 0 and last_h1['hist_h1'] > 0 and - last_h1['stoch_k'] > last_h1['stoch_d'] and - prev_h1['stoch_k'] <= prev_h1['stoch_d'] - ): + if (last_d1[macd_hist_col] > 0 and last_h1[macd_hist_col] > 0 and + last_h1[stoch_k_col] > last_h1[stoch_d_col] and prev_h1[stoch_k_col] <= prev_h1[stoch_d_col]): ta_suggestion = "BUY" - elif ( - last_d1['hist_d1'] < 0 and last_h1['hist_h1'] < 0 and - last_h1['stoch_k'] < last_h1['stoch_d'] and - prev_h1['stoch_k'] >= prev_h1['stoch_d'] - ): + elif (last_d1[macd_hist_col] < 0 and last_h1[macd_hist_col] < 0 and + last_h1[stoch_k_col] < last_h1[stoch_d_col] and prev_h1[stoch_k_col] >= prev_h1[stoch_d_col]): ta_suggestion = "SELL" # AI functionality is temporarily disabled. @@ -52,6 +71,6 @@ class MercyEdgeStrategy(BaseStrategy): return { "signal": final_signal, "price": last_h1["close"], "explanation": f"TA: {ta_suggestion} (AI disabled)", - "D1_MACDh": last_d1['hist_d1'], "H1_MACDh": last_h1['hist_h1'], - "H1_STOCHk": last_h1['stoch_k'], "H1_STOCHd": last_h1['stoch_d'] + "D1_MACDh": last_d1[macd_hist_col], "H1_MACDh": last_h1[macd_hist_col], + "H1_STOCHk": last_h1[stoch_k_col], "H1_STOCHd": last_h1[stoch_d_col] } diff --git a/core/strategies/quantumbotx_hybrid.py b/core/strategies/quantumbotx_hybrid.py index adc23b7..e43af73 100644 --- a/core/strategies/quantumbotx_hybrid.py +++ b/core/strategies/quantumbotx_hybrid.py @@ -8,6 +8,18 @@ class QuantumBotXHybridStrategy(BaseStrategy): name = 'QuantumBotX Hybrid' description = 'Strategi eksklusif yang menggabungkan beberapa indikator untuk performa optimal di berbagai kondisi pasar.' + @classmethod + def get_definable_params(cls): + """Mengembalikan parameter yang bisa diatur untuk strategi ini.""" + return [ + {"name": "adx_period", "label": "Periode ADX", "type": "number", "default": 14}, + {"name": "adx_threshold", "label": "Ambang ADX", "type": "number", "default": 25}, + {"name": "ma_fast_period", "label": "Periode MA Cepat", "type": "number", "default": 20}, + {"name": "ma_slow_period", "label": "Periode MA Lambat", "type": "number", "default": 50}, + {"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} + ] + def analyze(self): """ Menganalisis pasar menggunakan strategi Hybrid yang adaptif. @@ -16,21 +28,40 @@ class QuantumBotXHybridStrategy(BaseStrategy): """ tf_const = self.bot.tf_map.get(self.bot.timeframe, mt5.TIMEFRAME_H1) - # Butuh data yang cukup untuk indikator terpanjang (SMA 50) - df = get_rates(self.bot.market_for_mt5, tf_const, 52) + # PERBAIKAN: Minta lebih banyak data. Indikator kompleks seperti ADX + # butuh "pemanasan" lebih lama. 100 bar adalah angka yang lebih aman. + data_points_to_fetch = 100 + required_data_points = 51 # Tetap butuh minimal 51 untuk SMA(50) + df = get_rates(self.bot.market_for_mt5, tf_const, data_points_to_fetch) - if df is None or df.empty or len(df) < 51: - return {"signal": "HOLD", "price": None, "explanation": "Data tidak cukup untuk Hybrid."} + if df is None or df.empty or len(df) < required_data_points: + return {"signal": "HOLD", "price": None, "explanation": f"Data tidak cukup ({len(df) if df is not None else 0}/{required_data_points} bar)."} + + # --- Ambil Parameter Dinamis --- + adx_period = self.params.get('adx_period', 14) + adx_threshold = self.params.get('adx_threshold', 25) + ma_fast_period = self.params.get('ma_fast_period', 20) + ma_slow_period = self.params.get('ma_slow_period', 50) + bb_length = self.params.get('bb_length', 20) + bb_std = self.params.get('bb_std', 2.0) + + # PERBAIKAN: Paksa format float dengan satu desimal pada nama kolom + # untuk mencegah KeyError (misal: 'BBL_20_2' vs 'BBL_20_2.0') + bbu_col = f'BBU_{bb_length}_{bb_std:.1f}' + bbl_col = f'BBL_{bb_length}_{bb_std:.1f}' # --- Hitung SEMUA Indikator yang Dibutuhkan --- - df.ta.adx(length=14, append=True) - df['SMA_20'] = ta.sma(df['close'], length=20) - df['SMA_50'] = ta.sma(df['close'], length=50) - df.ta.bbands(length=20, std=2.0, append=True) + 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) + + # Simpan panjang sebelum dropna untuk debugging + len_before_drop = len(df) df.dropna(inplace=True) if len(df) < 2: - return {"signal": "HOLD", "price": None, "explanation": "Indikator belum matang."} + return {"signal": "HOLD", "price": None, "explanation": f"Indikator belum matang setelah dropna (dari {len_before_drop} menjadi {len(df)} bar)."} last = df.iloc[-1] prev = df.iloc[-2] @@ -40,27 +71,27 @@ class QuantumBotXHybridStrategy(BaseStrategy): market_mode = "N/A" # --- Logika "Wasit Pasar" (ADX) --- - adx_value = last['ADX_14'] + adx_value = last[f'ADX_{adx_period}'] # KONDISI 1: PASAR TRENDING - if adx_value > 25: + if adx_value > adx_threshold: market_mode = "Trending" explanation = f"Mode: {market_mode} (ADX {adx_value:.1f}). Menunggu Crossover." - if prev['SMA_20'] <= prev['SMA_50'] and last['SMA_20'] > last['SMA_50']: + if prev[f'SMA_{ma_fast_period}'] <= prev[f'SMA_{ma_slow_period}'] and last[f'SMA_{ma_fast_period}'] > last[f'SMA_{ma_slow_period}']: signal = "BUY" explanation = f"Mode: {market_mode} (ADX {adx_value:.1f}). Sinyal: Golden Cross." - elif prev['SMA_20'] >= prev['SMA_50'] and last['SMA_20'] < last['SMA_50']: + elif prev[f'SMA_{ma_fast_period}'] >= prev[f'SMA_{ma_slow_period}'] and last[f'SMA_{ma_fast_period}'] < last[f'SMA_{ma_slow_period}']: signal = "SELL" explanation = f"Mode: {market_mode} (ADX {adx_value:.1f}). Sinyal: Death Cross." # KONDISI 2: PASAR SIDEWAYS - elif adx_value < 25: + elif adx_value < adx_threshold: market_mode = "Ranging" explanation = f"Mode: {market_mode} (ADX {adx_value:.1f}). Menunggu pantulan Bands." - if last['low'] <= last['BBL_20_2.0']: + if last['low'] <= last[bbl_col]: signal = "BUY" explanation = f"Mode: {market_mode} (ADX {adx_value:.1f}). Sinyal: Oversold di Band Bawah." - elif last['high'] >= last['BBU_20_2.0']: + elif last['high'] >= last[bbu_col]: signal = "SELL" explanation = f"Mode: {market_mode} (ADX {adx_value:.1f}). Sinyal: Overbought di Band Atas." @@ -69,8 +100,8 @@ class QuantumBotXHybridStrategy(BaseStrategy): "price": price, "explanation": explanation, "Market_Mode": market_mode, - "ADX_14": adx_value, - "SMA_20": last.get('SMA_20'), - "SMA_50": last.get('SMA_50'), + f"ADX_{adx_period}": adx_value, + f"SMA_{ma_fast_period}": last.get(f'SMA_{ma_fast_period}'), + f"SMA_{ma_slow_period}": last.get(f'SMA_{ma_slow_period}'), } return analysis_data \ No newline at end of file diff --git a/core/strategies/rsi_breakout.py b/core/strategies/rsi_breakout.py index 999144c..1f2aab9 100644 --- a/core/strategies/rsi_breakout.py +++ b/core/strategies/rsi_breakout.py @@ -1,41 +1,56 @@ -# core/strategies/rsi_breakout.py +# /core/strategies/rsi_breakout.py import pandas_ta as ta import MetaTrader5 as mt5 from .base_strategy import BaseStrategy -from core.utils.mt5 import get_rates_from_mt5 +from core.data.fetch import get_rates class RSIBreakoutStrategy(BaseStrategy): name = 'RSI Breakout' - description = 'Sinyal berdasarkan level jenuh beli (overbought) dan jenuh jual (oversold) dari Relative Strength Index (RSI).' + description = 'Sinyal berdasarkan RSI yang keluar dari zona jenuh beli (overbought) atau jenuh jual (oversold).' + + @classmethod + def get_definable_params(cls): + """Mengembalikan parameter yang bisa diatur untuk strategi ini.""" + return [ + {"name": "rsi_period", "label": "Periode RSI", "type": "number", "default": 14}, + {"name": "overbought_level", "label": "Level Overbought", "type": "number", "default": 70}, + {"name": "oversold_level", "label": "Level Oversold", "type": "number", "default": 30} + ] def analyze(self): tf_const = self.bot.tf_map.get(self.bot.timeframe, mt5.TIMEFRAME_H1) - df = get_rates_from_mt5(self.bot.market_for_mt5, tf_const, 100) + + rsi_period = self.params.get('rsi_period', 14) + overbought_level = self.params.get('overbought_level', 70) + oversold_level = self.params.get('oversold_level', 30) - if df is None or df.empty or len(df) < 20: - return {"signal": "HOLD", "price": None, "explanation": "Data tidak cukup"} + df = get_rates(self.bot.market_for_mt5, tf_const, rsi_period + 5) - df["RSI"] = ta.rsi(df["close"], length=14) + if df is None or df.empty or len(df) < rsi_period + 2: + return {"signal": "HOLD", "price": None, "explanation": "Data tidak cukup untuk RSI."} + + df['RSI'] = ta.rsi(df['close'], length=rsi_period) df.dropna(inplace=True) - if len(df) < 1: - return {"signal": "HOLD", "price": None, "explanation": "Indikator belum matang."} + if len(df) < 2: + return {"signal": "HOLD", "price": None, "explanation": "Indikator RSI belum matang."} last = df.iloc[-1] + prev = df.iloc[-2] price = last["close"] - rsi = last["RSI"] - signal = "HOLD" - explanation = f"RSI saat ini {rsi:.2f}, dalam zona netral" + explanation = f"RSI ({last['RSI']:.2f}) berada di zona netral." - if rsi > 70: - signal = "SELL" - explanation = f"RSI {rsi:.2f} > 70 (overbought)" - elif rsi < 30: + # Sinyal Beli: RSI melintasi ke atas dari level oversold + if prev['RSI'] < oversold_level and last['RSI'] >= oversold_level: signal = "BUY" - explanation = f"RSI {rsi:.2f} < 30 (oversold)" + explanation = f"RSI Breakout NAIK! RSI ({last['RSI']:.2f}) melintasi ke atas level oversold ({oversold_level})." + # Sinyal Jual: RSI melintasi ke bawah dari level overbought + elif prev['RSI'] > overbought_level and last['RSI'] <= overbought_level: + signal = "SELL" + explanation = f"RSI Breakout TURUN! RSI ({last['RSI']:.2f}) melintasi ke bawah level overbought ({overbought_level})." return { "signal": signal, "price": price, "explanation": explanation, - "rsi": rsi - } + "RSI": last['RSI'], "Overbought_Level": overbought_level, "Oversold_Level": oversold_level + } \ No newline at end of file diff --git a/static/js/bot_detail.js b/static/js/bot_detail.js index d2e441e..e54f178 100644 --- a/static/js/bot_detail.js +++ b/static/js/bot_detail.js @@ -35,16 +35,29 @@ document.addEventListener('DOMContentLoaded', function() { botMarketHeader.textContent = `Pasar: ${botData.market} | Timeframe: ${botData.timeframe}`; botStatusBadge.textContent = botData.status; - // Render Parameter - paramsContainer.innerHTML = ` + // Render Parameter Standar + let paramsHTML = `

Lot Size

${botData.lot_size}

Stop Loss

${botData.sl_pips} pips

Take Profit

${botData.tp_pips} pips

Interval

${botData.check_interval_seconds}s

-

Strategi

${botData.strategy}

`; + + // Render Parameter Strategi Kustom jika ada + const customParams = botData.strategy_params || {}; // Backend sudah mengubahnya menjadi objek + const customParamKeys = Object.keys(customParams); + + if (customParamKeys.length > 0) { + paramsHTML += '

Parameter Strategi

'; + customParamKeys.forEach(key => { + const label = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); + paramsHTML += `

${label}

${customParams[key]}

`; + }); + paramsHTML += '
'; + } + paramsContainer.innerHTML = paramsHTML; } catch (e) { console.error('Error fetching bot details:', e); diff --git a/static/js/trading_bots.js b/static/js/trading_bots.js index 3102243..73e792f 100644 --- a/static/js/trading_bots.js +++ b/static/js/trading_bots.js @@ -8,11 +8,22 @@ document.addEventListener('DOMContentLoaded', function() { const modalTitle = document.getElementById('modal-title'); const createBotBtn = document.getElementById('create-bot-btn'); const cancelBtn = document.getElementById('cancel-create'); + const paramsContainer = document.getElementById('strategy-params-container'); const strategySelect = document.getElementById('strategy'); let currentBotId = null; // Variabel untuk melacak bot yang sedang diedit // --- Fungsi --- + // Fungsi untuk mengisi nilai parameter strategi saat mengedit bot + function fillStrategyParams(params) { + if (!params) return; + Object.entries(params).forEach(([key, value]) => { + const inputElement = document.getElementById(key); + if (inputElement) { + inputElement.value = value; + } + }); + } // Fungsi untuk memuat daftar strategi ke dalam form async function loadStrategies() { try { @@ -95,6 +106,7 @@ document.addEventListener('DOMContentLoaded', function() { // Buka modal untuk membuat bot baru createBotBtn.addEventListener("click", () => { currentBotId = null; + paramsContainer.innerHTML = ''; // Kosongkan parameter form.reset(); modalTitle.textContent = '๐Ÿš€ Buat Bot Baru'; // Set nilai default @@ -125,6 +137,14 @@ document.addEventListener('DOMContentLoaded', function() { } }); + // Kumpulkan parameter strategi dinamis + const params = {}; + const paramInputs = paramsContainer.querySelectorAll('input'); + paramInputs.forEach(input => { + params[input.name] = parseFloat(input.value) || input.value; + }); + data.params = params; + const url = currentBotId ? `/api/bots/${currentBotId}` : '/api/bots'; const method = currentBotId ? 'PUT' : 'POST'; @@ -170,6 +190,12 @@ document.addEventListener('DOMContentLoaded', function() { form.elements[key].value = bot[key]; } } + // Trigger perubahan strategi untuk memuat dan mengisi parameter + strategySelect.dispatchEvent(new Event('change', { 'bubbles': true })); + // Isi nilai parameter yang sudah ada + if (bot.strategy_params) { + setTimeout(() => fillStrategyParams(bot.strategy_params), 200); // Beri waktu untuk form dibuat + } modal.classList.remove('hidden'); } else { alert(`โŒ Gagal memuat data bot: ${bot.error}`); @@ -212,6 +238,39 @@ document.addEventListener('DOMContentLoaded', function() { } }); + // Event listener untuk dropdown strategi + strategySelect.addEventListener('change', async (e) => { + const strategyId = e.target.value; + paramsContainer.innerHTML = '

Memuat parameter...

'; + if (!strategyId) { + paramsContainer.innerHTML = ''; + return; + } + + try { + const res = await fetch(`/api/strategies/${strategyId}/params`); + const params = await res.json(); + paramsContainer.innerHTML = ''; // Kosongkan lagi + + if (params.length > 0) { + params.forEach(param => { + const paramField = ` +
+ + +
+ `; + paramsContainer.innerHTML += paramField; + }); + } else { + paramsContainer.innerHTML = '

Strategi ini tidak memiliki parameter kustom.

'; + } + } catch (err) { + console.error('Gagal memuat parameter strategi:', err); + paramsContainer.innerHTML = '

Gagal memuat parameter.

'; + } + }); // --- Panggilan Awal --- loadStrategies(); // Muat strategi saat halaman pertama kali dibuka diff --git a/templates/trading_bots.html b/templates/trading_bots.html index dda7e5d..78a220e 100644 --- a/templates/trading_bots.html +++ b/templates/trading_bots.html @@ -83,57 +83,90 @@ - -