diff --git a/app.py b/app.py index 58f339c..1e0b416 100644 --- a/app.py +++ b/app.py @@ -34,22 +34,22 @@ app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'your-secret-key-here') # --- Registrasi Blueprints --- # Impor blueprints setelah 'app' dibuat untuk menghindari circular import -from core.routes.api_dashboard import api_dashboard # noqa: E402 -from core.routes.api_chart import api_chart # noqa: E402 -from core.routes.api_bots import api_bots # noqa: E402 -from core.routes.api_profile import api_profile # noqa: E402 -from core.routes.api_portfolio import api_portfolio # noqa: E402 -from core.routes.api_history import api_history # noqa: E402 -from core.routes.api_notifications import api_notifications # noqa: E402 -from core.routes.api_stocks import api_stocks # noqa: E402 -from core.routes.api_forex import api_forex # noqa: E402 -from core.routes.api_crypto import api_crypto # noqa: E402 -from core.routes.api_fundamentals import api_fundamentals # noqa: E402 +from core.routes.api_dashboard import api_dashboard +from core.routes.api_chart import api_chart +from core.routes.api_bots import api_bots +from core.routes.api_profile import api_profile +from core.routes.api_portfolio import api_portfolio +from core.routes.api_history import api_history +from core.routes.api_notifications import api_notifications +from core.routes.api_stocks import api_stocks +from core.routes.api_forex import api_forex +from core.routes.api_fundamentals import api_fundamentals +from core.routes.api_backtest import api_backtest # Buat daftar semua blueprints untuk registrasi yang lebih rapi blueprints = [ api_dashboard, api_chart, api_bots, api_profile, api_portfolio, api_history, - api_notifications, api_stocks, api_forex, api_crypto, api_fundamentals + api_notifications, api_stocks, api_forex, api_fundamentals, api_backtest ] # Daftarkan setiap blueprint ke aplikasi @@ -59,48 +59,47 @@ for blueprint in blueprints: # --- Rute Halaman (Views) --- @app.route('/') def dashboard(): - return render_template('index.html') + return render_template('index.html', active_page='dashboard') @app.route('/trading_bots') def bots_page(): - return render_template('trading_bots.html') + return render_template('trading_bots.html', active_page='trading_bots') @app.route('/bots/') def bot_detail_page(bot_id): - return render_template('bot_detail.html') + return render_template('bot_detail.html', active_page='trading_bots') # Tetap di menu trading_bots + +@app.route('/backtesting') +def backtesting_page(): + return render_template('backtesting.html', active_page='backtesting') -# ... (rute-rute halaman lainnya tetap sama) ... @app.route('/portfolio') def portfolio_page(): - return render_template('portfolio.html') + return render_template('portfolio.html', active_page='portfolio') @app.route('/history') def history_page(): - return render_template('history.html') + return render_template('history.html', active_page='history') @app.route('/settings') def settings_page(): - return render_template('settings.html') + return render_template('settings.html', active_page='settings') @app.route('/profile') def profile_page(): - return render_template('profile.html') + return render_template('profile.html', active_page='profile') # Asumsi profile punya menu sendiri @app.route('/notifications') def notifications_page(): - return render_template('notifications.html') - -@app.route('/cryptocurrency') -def crypto_page(): - return render_template('cryptocurrency.html') + return render_template('notifications.html', active_page='notifications') @app.route('/stocks') def stocks_page(): - return render_template('stocks.html') + return render_template('stocks.html', active_page='stocks') @app.route('/forex') def forex_page(): - return render_template('forex.html') + return render_template('forex.html', active_page='forex') # --- Error Handlers & Rute Lain-lain --- @app.errorhandler(404) @@ -145,6 +144,21 @@ def shutdown_handler(): mt5.shutdown() # Pastikan koneksi MT5 selalu ditutup logger.info("Koneksi MetaTrader 5 ditutup. Proses shutdown selesai.") +# --- Filter Log Kustom --- +class RequestLogFilter(logging.Filter): + """ + Filter untuk menyembunyikan log permintaan (polling) yang tidak penting dari terminal. + """ + def filter(self, record): + # Pesan log dari Werkzeug terlihat seperti: "GET /api/path HTTP/1.1" 200 - + msg = record.getMessage() + # Daftar path yang ingin kita sembunyikan dari log konsol + paths_to_ignore = [ + "GET /api/notifications/unread-count", + "GET /api/bots/analysis" # <-- Ini juga sering di-poll + ] + return not any(path in msg for path in paths_to_ignore) + # --- Titik Eksekusi Utama --- if __name__ == '__main__': # Memuat kredensial MT5 dari .env dengan aman @@ -162,26 +176,34 @@ if __name__ == '__main__': exit(1) # Inisialisasi koneksi ke MetaTrader 5 - if not initialize_mt5(ACCOUNT, PASSWORD, SERVER): - logger.critical("GAGAL terhubung ke MetaTrader 5. Pastikan kredensial benar dan terminal berjalan.") - exit(1) - else: + # PERBAIKAN: Jangan langsung exit. Coba hubungkan, tapi biarkan aplikasi tetap berjalan jika gagal. + # Koneksi akan dicoba lagi saat bot pertama kali dimulai. + if initialize_mt5(ACCOUNT, PASSWORD, SERVER): logger.info("Berhasil terhubung ke MetaTrader 5.") - # Muat semua bot yang ada di database try: ambil_semua_bot() logger.info("Semua bot dari database berhasil dimuat.") except Exception as e: logger.error(f"Terjadi kesalahan saat memuat bot: {e}", exc_info=True) + else: + logger.warning("GAGAL terhubung ke MetaTrader 5 saat startup. Aplikasi akan berjalan tanpa koneksi live.") + logger.warning("Fitur bot live tidak akan berfungsi sampai koneksi MT5 pulih.") - # --- 3. Daftarkan fungsi shutdown --- - atexit.register(shutdown_handler) - - # Jalankan aplikasi Flask - app.run( - debug=os.getenv('FLASK_DEBUG', 'False').lower() == 'true', - host=os.getenv('FLASK_HOST', '127.0.0.1'), - port=int(os.getenv('FLASK_PORT', 5000)), - use_reloader=False # Penting: False untuk mencegah eksekusi ganda pada background thread - ) + # --- Terapkan Filter Log --- + # Dapatkan logger bawaan Werkzeug dan tambahkan filter kita + werkzeug_logger = logging.getLogger('werkzeug') + werkzeug_logger.addFilter(RequestLogFilter()) + + # --- 3. Daftarkan fungsi shutdown --- + # PERBAIKAN: Pindahkan ke luar blok if/else agar selalu dijalankan. + atexit.register(shutdown_handler) + + # Jalankan aplikasi Flask + # PERBAIKAN: Pindahkan ke luar blok if/else agar server selalu berjalan. + app.run( + debug=os.getenv('FLASK_DEBUG', 'False').lower() == 'true', + host=os.getenv('FLASK_HOST', '127.0.0.1'), + port=int(os.getenv('FLASK_PORT', 5000)), + use_reloader=False # Penting: False untuk mencegah eksekusi ganda pada background thread + ) diff --git a/core/backtesting/engine.py b/core/backtesting/engine.py new file mode 100644 index 0000000..8d5ad4b --- /dev/null +++ b/core/backtesting/engine.py @@ -0,0 +1,123 @@ +# core/backtesting/engine.py + +from core.strategies.strategy_map import STRATEGY_MAP + +def run_backtest(strategy_id, params, historical_data_df): + """ + Menjalankan simulasi backtesting untuk strategi tertentu pada data historis. + """ + strategy_class = STRATEGY_MAP.get(strategy_id) + if not strategy_class: + return {"error": "Strategi tidak ditemukan"} + + # Inisialisasi state backtesting + trades = [] + in_position = False + initial_capital = 10000 # Modal awal virtual $10,000 + capital = initial_capital + equity_curve = [initial_capital] + peak_equity = initial_capital + max_drawdown = 0.0 + + position_type = None + entry_price = 0.0 + sl_pips = params.get('sl_pips', 100) + tp_pips = params.get('tp_pips', 200) + + # Asumsi pip value sederhana untuk backtesting, bisa disempurnakan nanti + # Untuk pair JPY, point adalah 0.001, untuk yang lain 0.00001 + point = 0.001 if 'JPY' in historical_data_df.columns[0].upper() else 0.00001 + # Asumsi nilai per pip untuk 0.01 lot + # Ini adalah penyederhanaan besar, tapi cukup untuk backtesting awal + value_per_pip = 0.1 # $0.10 per pip + + pip_value = 10 * point + + # Mock bot object untuk strategi + class MockBot: + def __init__(self): + self.market_for_mt5 = "BACKTEST" + self.timeframe = "H1" + self.tf_map = {} + + # Inisialisasi strategi dengan parameter yang diberikan + strategy_instance = strategy_class(bot_instance=MockBot(), params=params) + + # Loop melalui setiap bar data historis + for i in range(1, len(historical_data_df)): + # Buat DataFrame "seolah-olah" ini adalah data real-time hingga bar saat ini + # PERBAIKAN: Gunakan .copy() untuk membuat salinan eksplisit dari slice. + # Ini akan menghilangkan SettingWithCopyWarning di semua strategi. + current_market_data = historical_data_df.iloc[:i].copy() + + analysis = strategy_instance.analyze(current_market_data) + signal = analysis.get("signal") + current_price = historical_data_df.iloc[i]['close'] + + # Cek SL/TP jika sedang dalam posisi + if in_position: + profit = 0 + if position_type == 'BUY': + profit_pips = (current_price - entry_price) / point / 10 + if current_price <= entry_price - (sl_pips * pip_value): + trades.append({'entry': entry_price, 'exit': current_price, 'profit_pips': profit_pips, 'reason': 'SL'}) + capital += profit_pips * value_per_pip + in_position = False + elif current_price >= entry_price + (tp_pips * pip_value): + trades.append({'entry': entry_price, 'exit': current_price, 'profit_pips': profit_pips, 'reason': 'TP'}) + capital += profit_pips * value_per_pip + in_position = False + elif position_type == 'SELL': + profit_pips = (entry_price - current_price) / point / 10 + if current_price >= entry_price + (sl_pips * pip_value): + trades.append({'entry': entry_price, 'exit': current_price, 'profit_pips': profit_pips, 'reason': 'SL'}) + capital += profit_pips * value_per_pip + in_position = False + elif current_price <= entry_price - (tp_pips * pip_value): + trades.append({'entry': entry_price, 'exit': current_price, 'profit_pips': profit_pips, 'reason': 'TP'}) + capital += profit_pips * value_per_pip + in_position = False + + if not in_position: # Jika posisi baru saja ditutup + equity_curve.append(capital) + peak_equity = max(peak_equity, capital) + drawdown = (peak_equity - capital) / peak_equity + max_drawdown = max(max_drawdown, drawdown) + + # Cek sinyal baru + if signal == 'BUY' and not in_position: + in_position = True + position_type = 'BUY' + entry_price = current_price + elif signal == 'SELL' and not in_position: + in_position = True + position_type = 'SELL' + entry_price = current_price + elif (signal == 'SELL' and in_position and position_type == 'BUY') or \ + (signal == 'BUY' and in_position and position_type == 'SELL'): + # Sinyal berlawanan, tutup posisi lama + profit_pips = ((current_price - entry_price) if position_type == 'BUY' else (entry_price - current_price)) / point / 10 + trades.append({'entry': entry_price, 'exit': current_price, 'profit_pips': profit_pips, 'reason': 'Signal Flip'}) + capital += profit_pips * value_per_pip + equity_curve.append(capital) + peak_equity = max(peak_equity, capital) + drawdown = (peak_equity - capital) / peak_equity + max_drawdown = max(max_drawdown, drawdown) + in_position = False + + # Hitung hasil akhir + total_profit_pips = sum(trade['profit_pips'] for trade in trades) + wins = len([trade for trade in trades if trade['profit_pips'] > 0]) + losses = len(trades) - wins + win_rate = (wins / len(trades) * 100) if trades else 0 + + return { + "total_trades": len(trades), + "total_profit_pips": total_profit_pips, + "win_rate_percent": win_rate, + "wins": wins, + "losses": losses, + "max_drawdown_percent": max_drawdown * 100, + "equity_curve": equity_curve, + "trades": trades[-20:] # Tampilkan 20 trade terakhir + } \ No newline at end of file diff --git a/core/bots/trading_bot.py b/core/bots/trading_bot.py index c57f37d..d4cca7d 100644 --- a/core/bots/trading_bot.py +++ b/core/bots/trading_bot.py @@ -68,9 +68,13 @@ class TradingBot(threading.Thread): time.sleep(self.check_interval) continue - # --- PERBAIKAN: Panggil metode analyze dari objek strategi --- - # Metode analyze tidak lagi memerlukan argumen - self.last_analysis = self.strategy_instance.analyze() + # --- PERBAIKAN: Bot sekarang yang mengambil data --- + from core.data.fetch import get_rates + tf_const = self.tf_map.get(self.timeframe, mt5.TIMEFRAME_H1) + # Ambil data yang cukup untuk strategi terkompleks (misal, 250 bar untuk EMA 200) + df = get_rates(self.market_for_mt5, tf_const, 250) + + self.last_analysis = self.strategy_instance.analyze(df) logger.info(f"Bot {self.id} [{self.strategy_name}] - Last Analysis: {self.last_analysis}") signal = self.last_analysis.get("signal", "HOLD") diff --git a/core/routes/api_backtest.py b/core/routes/api_backtest.py new file mode 100644 index 0000000..2ddd16b --- /dev/null +++ b/core/routes/api_backtest.py @@ -0,0 +1,30 @@ +# core/routes/api_backtest.py + +import pandas as pd +import json +from flask import Blueprint, request, jsonify +from core.backtesting.engine import run_backtest + +api_backtest = Blueprint('api_backtest', __name__) + +@api_backtest.route('/api/backtest/run', methods=['POST']) +def run_backtest_route(): + if 'file' not in request.files: + return jsonify({"error": "Tidak ada file data yang diunggah"}), 400 + + file = request.files['file'] + if file.filename == '': + return jsonify({"error": "Nama file kosong"}), 400 + + try: + # Baca data CSV yang diunggah + df = pd.read_csv(file, parse_dates=['time']) + + # Ambil parameter dari form data + strategy_id = request.form.get('strategy') + params = json.loads(request.form.get('params', '{}')) + + results = run_backtest(strategy_id, params, df) + return jsonify(results) + except Exception as e: + return jsonify({"error": f"Terjadi kesalahan saat backtesting: {str(e)}"}), 500 \ No newline at end of file diff --git a/core/routes/api_crypto.py b/core/routes/api_crypto.py deleted file mode 100644 index f11a834..0000000 --- a/core/routes/api_crypto.py +++ /dev/null @@ -1,39 +0,0 @@ -from flask import Blueprint, jsonify - -# Initialize blueprint -api_crypto = Blueprint('api_crypto', __name__) - -# Sample data for initial load -SAMPLE_CRYPTO_DATA = [ - { - 'name': 'Bitcoin', - 'symbol': 'BTC', - 'price': 'Rp 1.234,567,890.00', - 'change': '+2.34%', - 'market_cap': 'Rp 2.345,678,901,234.00' - }, - { - 'name': 'Ethereum', - 'symbol': 'ETH', - 'price': 'Rp 123,456,789.00', - 'change': '-1.23%', - 'market_cap': 'Rp 1.234,567,890,123.00' - } -] - -@api_crypto.route('/api/crypto') -def get_crypto_data(): - """Get cryptocurrency market data""" - try: - # Try to get real data from CMC - from core.utils.external import get_crypto_data_from_cmc - real_data = get_crypto_data_from_cmc() - - if real_data and len(real_data) > 0: - return jsonify(real_data) - - # Fallback to sample data if API fails - return jsonify(SAMPLE_CRYPTO_DATA) - except Exception as e: - # Return error response - return jsonify({'error': str(e)}), 500 diff --git a/core/strategies/base_strategy.py b/core/strategies/base_strategy.py index a2fbfae..463beef 100644 --- a/core/strategies/base_strategy.py +++ b/core/strategies/base_strategy.py @@ -1,27 +1,24 @@ # core/strategies/base_strategy.py -class BaseStrategy: +from abc import ABC, abstractmethod + +class BaseStrategy(ABC): """ Kelas dasar abstrak untuk semua strategi trading. Setiap strategi harus mewarisi kelas ini dan mengimplementasikan metode `analyze`. """ 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): + @abstractmethod + def analyze(self, df): """ Metode inti yang harus di-override oleh setiap strategi turunan. Metode ini harus mengembalikan sebuah dictionary yang berisi hasil analisis. + Menerima DataFrame sebagai input. """ - raise NotImplementedError("Setiap strategi harus mengimplementasikan metode `analyze()`.") + raise NotImplementedError("Setiap strategi harus mengimplementasikan metode `analyze(df)`.") @classmethod def get_definable_params(cls): diff --git a/core/strategies/bollinger_bands.py b/core/strategies/bollinger_bands.py index ff0c987..29a4c9f 100644 --- a/core/strategies/bollinger_bands.py +++ b/core/strategies/bollinger_bands.py @@ -1,7 +1,5 @@ # /core/strategies/bollinger_bands.py -import MetaTrader5 as mt5 from .base_strategy import BaseStrategy -from core.data.fetch import get_rates class BollingerBandsStrategy(BaseStrategy): name = 'Bollinger Bands Reversion' @@ -15,16 +13,11 @@ class BollingerBandsStrategy(BaseStrategy): {"name": "bb_std", "label": "Standar Deviasi BB", "type": "number", "default": 2.0, "step": 0.1} ] - def analyze(self): + def analyze(self, df): """ Menganalisis pasar menggunakan strategi Bollinger Bands® Mean Reversion. Ideal untuk pasar ranging yang volatil seperti EURUSD. """ - tf_const = self.bot.tf_map.get(self.bot.timeframe, mt5.TIMEFRAME_H1) - - # Butuh data yang cukup untuk BBands(20) - df = get_rates(self.bot.market_for_mt5, tf_const, 21) - if df is None or df.empty or len(df) < 21: return {"signal": "HOLD", "price": None, "explanation": "Data tidak cukup untuk Bollinger Bands."} diff --git a/core/strategies/bollinger_squeeze.py b/core/strategies/bollinger_squeeze.py index 2c79536..67729c8 100644 --- a/core/strategies/bollinger_squeeze.py +++ b/core/strategies/bollinger_squeeze.py @@ -1,9 +1,7 @@ # /core/strategies/bollinger_squeeze.py import pandas_ta as ta import numpy as np -import MetaTrader5 as mt5 from .base_strategy import BaseStrategy -from core.data.fetch import get_rates # <-- PERBAIKAN: Impor dari lokasi yang benar class BollingerSqueezeStrategy(BaseStrategy): name = 'Bollinger Squeeze Breakout' @@ -21,17 +19,12 @@ class BollingerSqueezeStrategy(BaseStrategy): {"name": "volume_factor", "label": "Faktor Volume", "type": "number", "default": 1.5, "step": 0.1} ] - def analyze(self): + def analyze(self, df): """ Menganalisis pasar menggunakan strategi Bollinger Band Squeeze. Strategi ini menunggu periode volatilitas rendah ("squeeze") lalu masuk saat harga breakout. """ - tf_const = self.bot.tf_map.get(self.bot.timeframe, mt5.TIMEFRAME_H1) - - # Butuh data yang cukup untuk rolling window squeeze (120) + BBands (20) - df = get_rates(self.bot.market_for_mt5, tf_const, 150) # <-- PERBAIKAN: Gunakan fungsi yang benar - if df is None or df.empty or len(df) < 121: return {"signal": "HOLD", "price": None, "explanation": "Data tidak cukup untuk Bollinger Squeeze."} diff --git a/core/strategies/ma_crossover.py b/core/strategies/ma_crossover.py index f902f44..13e4c2c 100644 --- a/core/strategies/ma_crossover.py +++ b/core/strategies/ma_crossover.py @@ -1,8 +1,6 @@ # /core/strategies/ma_crossover.py import pandas_ta as ta -import MetaTrader5 as mt5 from .base_strategy import BaseStrategy -from core.data.fetch import get_rates class MACrossoverStrategy(BaseStrategy): name = 'Moving Average Crossover' @@ -16,17 +14,11 @@ class MACrossoverStrategy(BaseStrategy): {"name": "slow_period", "label": "Periode MA Lambat", "type": "number", "default": 50} ] - def analyze(self): + def analyze(self, df): """ Menganalisis pasar menggunakan strategi Moving Average Crossover (20/50). Ideal untuk pasar dengan tren kuat seperti XAUUSD. """ - # Mengakses properti dari instance bot yang tersimpan - tf_const = self.bot.tf_map.get(self.bot.timeframe, mt5.TIMEFRAME_H1) - - # Butuh data yang cukup untuk MA 50 - df = get_rates(self.bot.market_for_mt5, tf_const, 52) - if df is None or df.empty or len(df) < 51: return {"signal": "HOLD", "price": None, "explanation": "Data tidak cukup untuk MA Crossover."} diff --git a/core/strategies/mercy_edge.py b/core/strategies/mercy_edge.py index 1eeb008..d6941a6 100644 --- a/core/strategies/mercy_edge.py +++ b/core/strategies/mercy_edge.py @@ -19,7 +19,7 @@ class MercyEdgeStrategy(BaseStrategy): {"name": "stoch_smooth", "label": "Stoch Smooth", "type": "number", "default": 3}, ] - def analyze(self): + def analyze(self, df_h1): # Ambil parameter dinamis macd_fast = self.params.get('macd_fast', 12) macd_slow = self.params.get('macd_slow', 26) @@ -28,9 +28,8 @@ class MercyEdgeStrategy(BaseStrategy): stoch_d = self.params.get('stoch_d', 3) stoch_smooth = self.params.get('stoch_smooth', 3) - # Gunakan fungsi get_rates yang terstandarisasi + # Ambil data sekunder (D1), data primer (H1) sudah diberikan 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"} diff --git a/core/strategies/pulse_sync.py b/core/strategies/pulse_sync.py index c7e53d8..9dc2257 100644 --- a/core/strategies/pulse_sync.py +++ b/core/strategies/pulse_sync.py @@ -1,17 +1,12 @@ # core/strategies/pulse_sync.py import pandas_ta as ta -import MetaTrader5 as mt5 from .base_strategy import BaseStrategy -from core.data.fetch import get_rates class PulseSyncStrategy(BaseStrategy): name = 'Pulse Sync (AI)' description = 'Menggunakan AI untuk menganalisis momentum harga berdasarkan Simple Moving Average (SMA).' - def analyze(self): - tf_const = self.bot.tf_map.get(self.bot.timeframe, mt5.TIMEFRAME_H1) - df = get_rates(self.bot.market_for_mt5, tf_const, 100) - + def analyze(self, df): if df is None or df.empty or len(df) < 30: return {"signal": "HOLD", "price": None, "explanation": "Data tidak cukup"} diff --git a/core/strategies/quantum_velocity.py b/core/strategies/quantum_velocity.py index 0664d3c..4bc4020 100644 --- a/core/strategies/quantum_velocity.py +++ b/core/strategies/quantum_velocity.py @@ -2,9 +2,7 @@ import pandas_ta as ta import numpy as np -import MetaTrader5 as mt5 from .base_strategy import BaseStrategy -from core.data.fetch import get_rates class QuantumVelocityStrategy(BaseStrategy): name = 'Quantum Velocity' @@ -21,15 +19,10 @@ class QuantumVelocityStrategy(BaseStrategy): {"name": "squeeze_factor", "label": "Faktor Squeeze", "type": "number", "default": 0.7, "step": 0.1}, ] - def analyze(self): + def analyze(self, df): """ Menganalisis pasar dengan filter tren jangka panjang dan pemicu breakout. """ - tf_const = self.bot.tf_map.get(self.bot.timeframe, mt5.TIMEFRAME_H1) - - # Butuh data yang cukup untuk EMA 200 - df = get_rates(self.bot.market_for_mt5, tf_const, 250) - if df is None or df.empty or len(df) < 201: return {"signal": "HOLD", "price": None, "explanation": "Data tidak cukup untuk Quantum Velocity."} diff --git a/core/strategies/quantumbotx_hybrid.py b/core/strategies/quantumbotx_hybrid.py index e43af73..59ae0e4 100644 --- a/core/strategies/quantumbotx_hybrid.py +++ b/core/strategies/quantumbotx_hybrid.py @@ -1,8 +1,6 @@ # /core/strategies/quantumbotx_hybrid.py import pandas_ta as ta -import MetaTrader5 as mt5 from .base_strategy import BaseStrategy -from core.data.fetch import get_rates class QuantumBotXHybridStrategy(BaseStrategy): name = 'QuantumBotX Hybrid' @@ -20,19 +18,13 @@ class QuantumBotXHybridStrategy(BaseStrategy): {"name": "bb_std", "label": "Std Dev BB", "type": "number", "default": 2.0, "step": 0.1} ] - def analyze(self): + def analyze(self, df): """ Menganalisis pasar menggunakan strategi Hybrid yang adaptif. Menggunakan MA Crossover saat trending (ADX > 25) dan Bollinger Bands saat ranging (ADX < 25). """ - tf_const = self.bot.tf_map.get(self.bot.timeframe, mt5.TIMEFRAME_H1) - - # 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) < 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)."} diff --git a/core/strategies/rsi_breakout.py b/core/strategies/rsi_breakout.py index 1f2aab9..5203bef 100644 --- a/core/strategies/rsi_breakout.py +++ b/core/strategies/rsi_breakout.py @@ -1,8 +1,6 @@ # /core/strategies/rsi_breakout.py import pandas_ta as ta -import MetaTrader5 as mt5 from .base_strategy import BaseStrategy -from core.data.fetch import get_rates class RSIBreakoutStrategy(BaseStrategy): name = 'RSI Breakout' @@ -17,15 +15,11 @@ class RSIBreakoutStrategy(BaseStrategy): {"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) - + def analyze(self, df): rsi_period = self.params.get('rsi_period', 14) overbought_level = self.params.get('overbought_level', 70) oversold_level = self.params.get('oversold_level', 30) - df = get_rates(self.bot.market_for_mt5, tf_const, rsi_period + 5) - if df is None or df.empty or len(df) < rsi_period + 2: return {"signal": "HOLD", "price": None, "explanation": "Data tidak cukup untuk RSI."} diff --git a/core/data/crypto_data.py b/engine.py similarity index 100% rename from core/data/crypto_data.py rename to engine.py diff --git a/static/js/backtesting.js b/static/js/backtesting.js new file mode 100644 index 0000000..528496e --- /dev/null +++ b/static/js/backtesting.js @@ -0,0 +1,170 @@ +// static/js/backtesting.js +document.addEventListener('DOMContentLoaded', () => { + const strategySelect = document.getElementById('strategy-select'); + const paramsContainer = document.getElementById('params-container'); + const form = document.getElementById('backtest-form'); + const runBtn = document.getElementById('run-backtest-btn'); + const resultsContainer = document.getElementById('results-container'); + const resultsSummary = document.getElementById('results-summary'); + const loadingSpinner = document.getElementById('loading-spinner'); + let equityChart = null; // Variabel untuk menyimpan instance grafik + const resultsLog = document.getElementById('results-log'); + + // Muat strategi ke dropdown + async function loadStrategies() { + try { + const response = await fetch('/api/strategies'); + if (!response.ok) throw new Error('Gagal memuat strategi.'); + const strategies = await response.json(); + + strategySelect.innerHTML = ''; + strategies.forEach(strategy => { + const option = document.createElement('option'); + option.value = strategy.id; + option.textContent = strategy.name; + strategySelect.appendChild(option); + }); + } catch (error) { + console.error('Error loading strategies:', error); + strategySelect.innerHTML = ''; + } + } + + // Muat parameter saat strategi dipilih + strategySelect.addEventListener('change', async () => { + const strategyId = strategySelect.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 => { + paramsContainer.innerHTML += ` +
+ + +
`; + }); + } else { + paramsContainer.innerHTML = '

Strategi ini tidak memiliki parameter kustom.

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

Gagal memuat parameter.

'; + } + }); + + // Jalankan backtest saat form disubmit + form.addEventListener('submit', async (e) => { + e.preventDefault(); + loadingSpinner.classList.remove('hidden'); + resultsContainer.classList.add('hidden'); + runBtn.disabled = true; + runBtn.textContent = 'Menjalankan...'; + + const formData = new FormData(form); + + // Kumpulkan parameter kustom + const params = {}; + // Tambahkan parameter SL/TP yang mungkin tidak ada di form kustom + params['sl_pips'] = 100; // Nilai default, bisa dibuat dinamis nanti + params['tp_pips'] = 200; // Nilai default + + paramsContainer.querySelectorAll('input').forEach(input => { + const value = parseFloat(input.value); + // Pastikan hanya angka valid yang di-parse, selain itu ambil string + params[input.name] = isNaN(value) ? input.value : value; + }); + formData.append('params', JSON.stringify(params)); + + try { + const response = await fetch('/api/backtest/run', { + method: 'POST', + body: formData, // Kirim sebagai multipart/form-data + }); + const results = await response.json(); + + if (response.ok) { + displayResults(results); + } else { + alert(`Error: ${results.error}`); + } + } catch (err) { + console.error("Backtest failed:", err); + alert('Gagal terhubung ke server.'); + } finally { + loadingSpinner.classList.add('hidden'); + runBtn.disabled = false; + runBtn.textContent = 'Jalankan Backtest'; + } + }); + + function displayResults(data) { + resultsContainer.classList.remove('hidden'); + // PERBAIKAN: Tampilkan 6 metrik utama + resultsSummary.innerHTML = ` +

Total Profit

${data.total_profit_pips.toFixed(2)} pips

+

Max Drawdown

${data.max_drawdown_percent.toFixed(2)}%

+

Win Rate

${data.win_rate_percent.toFixed(2)}%

+

Total Trades

${data.total_trades}

+

Wins

${data.wins}

+

Losses

${data.losses}

+ `; + + // Tampilkan grafik kurva ekuitas + displayEquityChart(data.equity_curve); + + // Tampilkan log trade (opsional) + if (data.trades && data.trades.length > 0) { + let logHtml = '

20 Trade Terakhir

'; + data.trades.forEach(trade => { + const profitClass = trade.profit_pips > 0 ? 'text-green-600' : 'text-red-600'; + logHtml += `

Entry: ${trade.entry.toFixed(4)} | Exit: ${trade.exit.toFixed(4)} | Profit: ${trade.profit_pips.toFixed(2)} pips | Reason: ${trade.reason}

`; + }); + logHtml += '
'; + resultsLog.innerHTML = logHtml; + } else { + resultsLog.innerHTML = ''; + } + } + + function displayEquityChart(equityData) { + const ctx = document.getElementById('equity-chart').getContext('2d'); + if (equityChart) { + equityChart.destroy(); // Hancurkan grafik lama sebelum membuat yang baru + } + equityChart = new Chart(ctx, { + type: 'line', + data: { + labels: Array.from({ length: equityData.length }, (_, i) => i + 1), // Label 1, 2, 3, ... + datasets: [{ + label: 'Equity Curve', + data: equityData, + borderColor: 'rgb(59, 130, 246)', + backgroundColor: 'rgba(59, 130, 246, 0.1)', + borderWidth: 2, + fill: true, + tension: 0.1, + pointRadius: 0, + }] + }, + options: { + responsive: true, + plugins: { + legend: { display: false }, + title: { display: true, text: 'Pertumbuhan Modal (Equity Curve)' } + }, + scales: { y: { beginAtZero: false } } + } + }); + } + + loadStrategies(); +}); diff --git a/static/js/cryptocurrency.js b/static/js/cryptocurrency.js deleted file mode 100644 index b828781..0000000 --- a/static/js/cryptocurrency.js +++ /dev/null @@ -1,42 +0,0 @@ -document.getElementById('sidebar-toggle').addEventListener('click', function() { - document.getElementById('sidebar').classList.toggle('collapsed'); -}); - -async function fetchCryptoData() { - const tableBody = document.querySelector('tbody'); - - try { - const response = await fetch('/api/crypto'); - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`); - } - const cryptos = await response.json(); - - if (!cryptos || cryptos.length === 0) { - tableBody.innerHTML = 'Tidak ada data cryptocurrency yang tersedia.'; - return; - } - - const rowsHtml = cryptos.map(crypto => { - const changeClass = crypto.change.startsWith('+') ? 'text-green-600' : 'text-red-600'; - return ` - -
${crypto.name}
${crypto.symbol}
- ${crypto.price} - ${crypto.change} - ${crypto.market_cap} - - - `; - }).join(''); - - tableBody.innerHTML = rowsHtml; - } catch (error) { - console.error('Gagal mengambil data crypto:', error); - tableBody.innerHTML = 'Gagal memuat data.'; - } -} - -document.addEventListener('DOMContentLoaded', fetchCryptoData); -setInterval(fetchCryptoData, 15000); diff --git a/templates/500.html b/templates/500.html new file mode 100644 index 0000000..1720800 --- /dev/null +++ b/templates/500.html @@ -0,0 +1,27 @@ + + + + + + + Terjadi Kesalahan Internal - QuantumBotX + + + + + + +
+

500

+

Terjadi Kesalahan Internal

+

+ Maaf, terjadi masalah pada server kami. Tim kami telah diberitahu dan sedang menanganinya. +

+ +
+ + \ No newline at end of file diff --git a/templates/backtesting.html b/templates/backtesting.html new file mode 100644 index 0000000..45703b7 --- /dev/null +++ b/templates/backtesting.html @@ -0,0 +1,50 @@ + +{% extends "base.html" %} + +{% block title %}Backtester{% endblock %} + +{% block content %} +

Strategy Backtester

+ +
+ +
+
+
+
+ + +
+
+ + +
+
+ +
+ +
+
+
+ + + + +
+{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..101e018 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,76 @@ + + + + + + {% block title %}Dashboard{% endblock %} - QuantumBotX + + + + + {% block head_extra %}{% endblock %} + + +
+ + + + +
+
+
+
+ +
+ + +
+
+
+ + +
+
+
+ + +
+ {% block content %}{% endblock %} +
+
+
+ + + + + {% block scripts %}{% endblock %} + + \ No newline at end of file diff --git a/templates/bot_detail.html b/templates/bot_detail.html index 882c172..6e85d73 100644 --- a/templates/bot_detail.html +++ b/templates/bot_detail.html @@ -1,108 +1,48 @@ - - - - - - - Detail Bot - QuantumBotX - - - - - -
- - - -
-
-
-
- -
-
- - -
-
-
+{% block title %}Detail Bot{% endblock %} - -
- -
-
-

Memuat...

-

Memuat detail bot...

-
-
Memuat Status...
-
+{% block content %} + +
+
+

Memuat...

+

Memuat detail bot...

+
+
Memuat Status...
+
- -
- -
-

Log Aktivitas

-
-

Memuat riwayat...

-
-
- - -
- -
-

Parameter

-
-

Memuat...

-
-
-
-

Analisis Real-Time

-
-

Memuat...

-
-
-
-
-
-
-
+ +
+ +
+

Log Aktivitas

+
+

Memuat riwayat...

- - - - + +
+ +
+

Parameter

+
+

Memuat...

+
+
+
+

Analisis Real-Time

+
+

Memuat...

+
+
-
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/templates/cryptocurrency.html b/templates/cryptocurrency.html deleted file mode 100644 index 52cf703..0000000 --- a/templates/cryptocurrency.html +++ /dev/null @@ -1,94 +0,0 @@ - - - - - - Cryptocurrency Market - QuantumBotX - - - - - -
- - - -
-
-
-
- -
-
-
- - - - - -
-
-
- -
-
-
-

Pasar Cryptocurrency

-

Data harga real-time dari pasar kripto global.

-
-
- - -
-
- -
- - - - - - - - - - - - - -
AsetHargaPerubahan 24jKap. PasarAksi
Memuat data...
-
-
-
-
- - - diff --git a/templates/forex.html b/templates/forex.html index d69962d..8520be3 100644 --- a/templates/forex.html +++ b/templates/forex.html @@ -1,108 +1,51 @@ - - - - - - Forex Market - QuantumBotX - - - - - -
- - -
-
-
-
- -
-
-
- - - - - -
-
-
- -
-
-
-

Pasar Forex

-

Data harga real-time dari pasar valuta asing.

-
-
+ +{% extends "base.html" %} -
- - - - - - - - - - - - - -
PasanganHarga BidHarga AskSpreadAksi
Memuat data forex...
-
-
-
+{% block title %}Forex Market{% endblock %} + +{% block content %} +
+
+

Pasar Forex

+

Data harga real-time dari pasar valuta asing.

- - -