mirror of
https://github.com/chrisnov-it/quantumbotx.git
synced 2026-07-28 19:27:44 +00:00
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.
This commit is contained in:
@@ -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',
|
||||
|
||||
+21
-3
@@ -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."
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+9
-18
@@ -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
|
||||
+7
-7
@@ -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:
|
||||
|
||||
+22
-1
@@ -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/<strategy_id>/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)
|
||||
|
||||
@@ -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/<int:bot_id>/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
|
||||
|
||||
|
||||
@@ -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()`.")
|
||||
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 []
|
||||
@@ -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
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
+16
-3
@@ -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 = `
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div><p class="text-gray-500">Lot Size</p><p class="font-semibold text-gray-800">${botData.lot_size}</p></div>
|
||||
<div><p class="text-gray-500">Stop Loss</p><p class="font-semibold text-gray-800">${botData.sl_pips} pips</p></div>
|
||||
<div><p class="text-gray-500">Take Profit</p><p class="font-semibold text-gray-800">${botData.tp_pips} pips</p></div>
|
||||
<div><p class="text-gray-500">Interval</p><p class="font-semibold text-gray-800">${botData.check_interval_seconds}s</p></div>
|
||||
<div><p class="text-gray-500">Strategi</p><p class="font-semibold text-gray-800">${botData.strategy}</p></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 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 += '<div class="border-t mt-4 pt-3"><h4 class="text-sm font-semibold text-gray-700 mb-2">Parameter Strategi</h4><div class="grid grid-cols-2 gap-4">';
|
||||
customParamKeys.forEach(key => {
|
||||
const label = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
||||
paramsHTML += `<div><p class="text-gray-500">${label}</p><p class="font-semibold text-gray-800">${customParams[key]}</p></div>`;
|
||||
});
|
||||
paramsHTML += '</div></div>';
|
||||
}
|
||||
paramsContainer.innerHTML = paramsHTML;
|
||||
|
||||
} catch (e) {
|
||||
console.error('Error fetching bot details:', e);
|
||||
|
||||
@@ -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 = '<p class="text-sm text-gray-500">Memuat parameter...</p>';
|
||||
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 = `
|
||||
<div class="col-span-1">
|
||||
<label for="${param.name}" class="block text-sm font-medium text-gray-700">${param.label}</label>
|
||||
<input type="${param.type || 'number'}" name="${param.name}" id="${param.name}" value="${param.default}" step="${param.step || 'any'}"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
|
||||
</div>
|
||||
`;
|
||||
paramsContainer.innerHTML += paramField;
|
||||
});
|
||||
} else {
|
||||
paramsContainer.innerHTML = '<p class="text-sm text-gray-500 col-span-2">Strategi ini tidak memiliki parameter kustom.</p>';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Gagal memuat parameter strategi:', err);
|
||||
paramsContainer.innerHTML = '<p class="text-sm text-red-500 col-span-2">Gagal memuat parameter.</p>';
|
||||
}
|
||||
});
|
||||
|
||||
// --- Panggilan Awal ---
|
||||
loadStrategies(); // Muat strategi saat halaman pertama kali dibuka
|
||||
|
||||
+81
-48
@@ -83,57 +83,90 @@
|
||||
</table>
|
||||
</div>
|
||||
</main>
|
||||
<!-- Modal Elegan Buat Bot -->
|
||||
<div id="create-bot-modal" class="fixed inset-0 z-50 hidden items-center justify-center bg-black bg-opacity-60 backdrop-blur-sm flex">
|
||||
<div class="bg-white shadow-2xl rounded-xl p-6 w-full max-w-md animate-fadeIn border border-blue-300">
|
||||
<h2 id="modal-title" class="text-xl font-bold mb-4 text-gray-800 border-b pb-2">🚀 Buat Bot Baru</h2>
|
||||
<form id="create-bot-form" class="space-y-4 text-sm">
|
||||
<div>
|
||||
<label for="name" class="block text-xs font-medium text-gray-600 mb-1">Nama Bot</label>
|
||||
<input type="text" id="name" name="name" placeholder="Contoh: Scalper Emas" class="w-full p-2 border rounded focus:ring focus:ring-blue-300" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="market" class="block text-xs font-medium text-gray-600 mb-1">Pasar</label>
|
||||
<input type="text" id="market" name="market" placeholder="Contoh: XAU/USD atau BTC/USDT" class="w-full p-2 border rounded focus:ring focus:ring-blue-300" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="strategy" class="block text-xs font-medium text-gray-600 mb-1">Strategi</label>
|
||||
<select name="strategy" id="strategy" class="w-full p-2 border rounded focus:ring focus:ring-blue-300">
|
||||
<!-- Options will be populated by JavaScript -->
|
||||
</select>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="lot_size" class="block text-xs font-medium text-gray-600 mb-1">Ukuran Lot</label>
|
||||
<input type="number" id="lot_size" name="lot_size" placeholder="0.01" step="0.01" value="0.01" class="w-full p-2 border rounded focus:ring focus:ring-blue-300" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="timeframe" class="block text-xs font-medium text-gray-600 mb-1">Timeframe</label>
|
||||
<input type="text" id="timeframe" name="timeframe" placeholder="H1" value="H1" class="w-full p-2 border rounded focus:ring focus:ring-blue-300" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="sl_pips" class="block text-xs font-medium text-gray-600 mb-1">Stop Loss (pips)</label>
|
||||
<input type="number" id="sl_pips" name="sl_pips" placeholder="100" value="100" class="w-full p-2 border rounded focus:ring focus:ring-blue-300" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="tp_pips" class="block text-xs font-medium text-gray-600 mb-1">Take Profit (pips)</label>
|
||||
<input type="number" id="tp_pips" name="tp_pips" placeholder="200" value="200" class="w-full p-2 border rounded focus:ring focus:ring-blue-300" required>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="check_interval_seconds" class="block text-xs font-medium text-gray-600 mb-1">Interval Cek (detik)</label>
|
||||
<input type="number" id="check_interval_seconds" name="check_interval_seconds" placeholder="60" value="60" class="w-full p-2 border rounded focus:ring focus:ring-blue-300" required>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3 pt-2">
|
||||
<button type="button" id="cancel-create" class="bg-gray-200 hover:bg-gray-300 text-sm px-4 py-2 rounded">Batal</button>
|
||||
<button type="submit" class="bg-blue-600 hover:bg-blue-700 text-white text-sm px-4 py-2 rounded shadow-lg">Buat Bot</button>
|
||||
<!-- Modal untuk Membuat/Mengedit Bot -->
|
||||
<div id="create-bot-modal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full flex items-center justify-center z-50 p-4">
|
||||
<!-- PERBAIKAN: Panel modal kini menggunakan flexbox vertikal dan tinggi maksimal -->
|
||||
<div class="relative bg-white rounded-lg shadow-xl w-full max-w-2xl flex flex-col max-h-[90vh]">
|
||||
|
||||
<!-- Header Modal (Tidak akan ikut ter-scroll) -->
|
||||
<div class="flex-shrink-0 flex items-start justify-between p-5 border-b rounded-t">
|
||||
<h3 id="modal-title" class="text-xl font-semibold text-gray-900">
|
||||
🚀 Buat Bot Baru
|
||||
</h3>
|
||||
<button type="button" id="cancel-create" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center">
|
||||
<i class="fas fa-times"></i>
|
||||
<span class="sr-only">Tutup modal</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Form (Area ini yang akan bisa di-scroll) -->
|
||||
<form id="create-bot-form" class="flex-grow overflow-y-auto">
|
||||
<div class="p-6 space-y-6">
|
||||
<!-- Baris 1: Nama & Pasar -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label for="name" class="block mb-2 text-sm font-medium text-gray-900">Nama Bot</label>
|
||||
<input type="text" name="name" id="name" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" placeholder="Contoh: XAUUSD Hybrid H1" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="market" class="block mb-2 text-sm font-medium text-gray-900">Pasar</label>
|
||||
<input type="text" name="market" id="market" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" placeholder="Contoh: XAUUSD atau EURUSD" required>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Baris 2: Lot, SL, TP -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div>
|
||||
<label for="lot_size" class="block mb-2 text-sm font-medium text-gray-900">Ukuran Lot</label>
|
||||
<input type="number" name="lot_size" id="lot_size" value="0.01" step="0.01" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="sl_pips" class="block mb-2 text-sm font-medium text-gray-900">Stop Loss (pips)</label>
|
||||
<input type="number" name="sl_pips" id="sl_pips" value="100" step="1" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="tp_pips" class="block mb-2 text-sm font-medium text-gray-900">Take Profit (pips)</label>
|
||||
<input type="number" name="tp_pips" id="tp_pips" value="200" step="1" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" required>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Baris 3: Timeframe & Interval -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label for="timeframe" class="block mb-2 text-sm font-medium text-gray-900">Timeframe</label>
|
||||
<select id="timeframe" name="timeframe" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
|
||||
<option value="M1">1 Menit</option>
|
||||
<option value="M5">5 Menit</option>
|
||||
<option value="M15">15 Menit</option>
|
||||
<option value="M30">30 Menit</option>
|
||||
<option value="H1" selected>1 Jam</option>
|
||||
<option value="H4">4 Jam</option>
|
||||
<option value="D1">1 Hari</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="check_interval_seconds" class="block mb-2 text-sm font-medium text-gray-900">Interval Cek (detik)</label>
|
||||
<input type="number" name="check_interval_seconds" id="check_interval_seconds" value="60" step="1" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" required>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Baris 4: Strategi & Parameternya -->
|
||||
<div>
|
||||
<label for="strategy" class="block mb-2 text-sm font-medium text-gray-900">Strategi</label>
|
||||
<select id="strategy" name="strategy" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" required>
|
||||
<option value="" disabled selected>Pilih sebuah strategi</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="strategy-params-container" class="grid grid-cols-1 md:grid-cols-2 gap-6 pt-4 border-t">
|
||||
<!-- Parameter strategi akan dimuat di sini oleh JavaScript -->
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- 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="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>
|
||||
</div>
|
||||
|
||||
<script src="{{ url_for('static', filename='js/trading_bots.js') }}"></script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user