From 243bdda80cbd97ce4710bd5c9ee57edf125cf5ab Mon Sep 17 00:00:00 2001 From: Reynov Christian Date: Sat, 23 Aug 2025 12:11:32 +0800 Subject: [PATCH] Enhance backtesting with dynamic position sizing and ATR-based SL/TP ``` The commit introduces significant improvements to the backtesting functionality: 1. Added dynamic position sizing based on risk percentage 2. Implemented ATR-based stop loss and take profit calculations 3. Enhanced backtest state management with detailed trade tracking 4. Updated database schema to store additional backtest metrics 5. Modified UI components to display new backtest results 6. Added logging and error handling improvements 7. Removed the London Breakout strategy from the codebase These changes improve the depth and accuracy of backtesting while providing more comprehensive performance metrics. --- core/backtesting/engine.py | 182 ++++++++++++++----------- core/bots/controller.py | 5 +- core/bots/trading_bot.py | 8 +- core/db/queries.py | 158 +--------------------- core/mt5/trade.py | 125 +++++++++++------ core/routes/api_backtest.py | 37 +++++- core/routes/api_bots.py | 2 +- core/strategies/london_breakout.py | 206 ----------------------------- core/strategies/strategy_map.py | 4 +- init_db.py | 74 +++++++---- static/js/backtest_history.js | 175 +++++++++--------------- static/js/backtesting.js | 6 +- static/js/bot_detail.js | 6 +- static/js/trading_bots.js | 13 +- templates/trading_bots.html | 4 +- 15 files changed, 362 insertions(+), 643 deletions(-) delete mode 100644 core/strategies/london_breakout.py diff --git a/core/backtesting/engine.py b/core/backtesting/engine.py index 6f75bdf..84423d0 100644 --- a/core/backtesting/engine.py +++ b/core/backtesting/engine.py @@ -1,12 +1,14 @@ # core/backtesting/engine.py -import pandas_ta as ta +import math # Import modul math +import logging # Import modul logging from core.strategies.strategy_map import STRATEGY_MAP +logger = logging.getLogger(__name__) + def run_backtest(strategy_id, params, historical_data_df): """ - Menjalankan simulasi backtesting untuk strategi tertentu pada data historis. - VERSI BARU: Menggunakan SL/TP dinamis berbasis ATR. + Menjalankan simulasi backtesting dengan position sizing dinamis. """ strategy_class = STRATEGY_MAP.get(strategy_id) if not strategy_class: @@ -15,28 +17,25 @@ def run_backtest(strategy_id, params, historical_data_df): # --- LANGKAH 1: Pra-perhitungan Indikator & ATR --- class MockBot: def __init__(self): - self.market_for_mt5 = "BACKTEST" + # Dapatkan nama simbol dari data historis + self.market_for_mt5 = historical_data_df.columns[0].split('_')[0] self.timeframe = "H1" self.tf_map = {} strategy_instance = strategy_class(bot_instance=MockBot(), params=params) - df_with_signals = strategy_instance.analyze_df(historical_data_df.copy()) - - # Hitung ATR untuk SL/TP dinamis + df = historical_data_df.copy() + df_with_signals = strategy_instance.analyze_df(df) df_with_signals.ta.atr(length=14, append=True) - # Hapus baris dengan nilai NaN setelah perhitungan indikator df_with_signals.dropna(inplace=True) - df_with_signals.reset_index(inplace=True) # Pastikan kita bisa iterasi dengan iloc + df_with_signals.reset_index(inplace=True) if df_with_signals.empty: - return {"error": "Gagal menghasilkan data indikator/ATR. Periksa panjang data input."} + return {"error": "Data tidak cukup untuk analisa."} - strategy_name = strategy_instance.name - - # --- LANGKAH 2: Inisialisasi state backtesting --- + # --- LANGKAH 2: Inisialisasi state & parameter --- trades = [] in_position = False - initial_capital = 10000 + initial_capital = 10000.0 capital = initial_capital equity_curve = [initial_capital] peak_equity = initial_capital @@ -44,106 +43,137 @@ def run_backtest(strategy_id, params, historical_data_df): position_type = None entry_price = 0.0 - entry_time = None sl_price = 0.0 tp_price = 0.0 + lot_size = 0.0 + entry_time = None # Inisialisasi entry_time - # Ambil multiplier dari params. Nama kunci masih 'sl_pips' & 'tp_pips' untuk konsistensi dengan DB. - # Konversi ke float untuk memastikan kalkulasi berjalan baik + risk_percent = float(params.get('lot_size', 1.0)) sl_atr_multiplier = float(params.get('sl_pips', 2.0)) tp_atr_multiplier = float(params.get('tp_pips', 4.0)) # --- LANGKAH 3: Loop melalui data --- for i in range(1, len(df_with_signals)): current_bar = df_with_signals.iloc[i] - - # Cek SL/TP jika sedang dalam posisi + + # Hentikan backtest jika modal habis + if capital <= 0: + break + if in_position: exit_price = None - reason = '' - - if position_type == 'BUY': - # Cek SL - if current_bar['low'] <= sl_price: - exit_price = sl_price - reason = 'SL' - # Cek TP - elif current_bar['high'] >= tp_price: - exit_price = tp_price - reason = 'TP' - - elif position_type == 'SELL': - # Cek SL - if current_bar['high'] >= sl_price: - exit_price = sl_price - reason = 'SL' - # Cek TP - elif current_bar['low'] <= tp_price: - exit_price = tp_price - reason = 'TP' + if position_type == 'BUY' and current_bar['low'] <= sl_price: exit_price = sl_price + elif position_type == 'BUY' and current_bar['high'] >= tp_price: exit_price = tp_price + elif position_type == 'SELL' and current_bar['high'] >= sl_price: exit_price = sl_price + elif position_type == 'SELL' and current_bar['low'] <= tp_price: exit_price = tp_price - # Proses penutupan posisi jika SL/TP tercapai if exit_price is not None: - # Asumsi 1 lot standar untuk kalkulasi profit/loss - profit = (exit_price - entry_price) if position_type == 'BUY' else (entry_price - exit_price) + # Tentukan ukuran kontrak berdasarkan simbol + contract_size = 100 if 'XAU' in strategy_instance.bot.market_for_mt5.upper() else 100000 + + # Profit calculation needs to account for scaled prices in commodities + symbol = strategy_instance.bot.market_for_mt5.upper() + if 'XAU' in symbol or 'XAG' in symbol: + point_value = 0.01 + profit_multiplier = lot_size * contract_size * point_value + else: + profit_multiplier = lot_size * contract_size + + if position_type == 'BUY': + profit = (exit_price - entry_price) * profit_multiplier + else: # SELL + profit = (entry_price - exit_price) * profit_multiplier + + # Pastikan profit adalah angka yang valid + if not math.isfinite(profit): + profit = 0.0 + + capital += profit trades.append({ - 'entry_time': str(entry_time), # Lebih aman dari strftime - 'exit_time': str(current_bar['time']), # Lebih aman dari strftime - 'entry': entry_price, - 'exit': exit_price, - 'profit_pips': profit, - 'reason': reason, + 'entry_time': str(entry_time), + 'exit_time': str(current_bar['time']), + 'entry': entry_price, + 'exit': exit_price, + 'profit': profit, + 'reason': 'SL/TP', # Default reason 'position_type': position_type }) - capital += profit equity_curve.append(capital) peak_equity = max(peak_equity, capital) drawdown = (peak_equity - capital) / peak_equity if peak_equity > 0 else 0 max_drawdown = max(max_drawdown, drawdown) in_position = False - position_type = None - # Cek sinyal baru (hanya jika tidak ada posisi) if not in_position: signal = current_bar.get("signal", "HOLD") - if signal == 'BUY' or signal == 'SELL': + if signal in ['BUY', 'SELL']: + entry_price = current_bar['close'] + entry_time = current_bar['time'] # Tambahkan baris ini + atr_value = current_bar['ATRr_14'] + if atr_value <= 0: + continue + + sl_distance = atr_value * sl_atr_multiplier + tp_distance = atr_value * tp_atr_multiplier + + if signal == 'BUY': + sl_price = entry_price - sl_distance + tp_price = entry_price + tp_distance + else: + sl_price = entry_price + sl_distance + tp_price = entry_price - tp_distance + + # Kalkulasi Lot Size + amount_to_risk = capital * (risk_percent / 100.0) + contract_size = 100 if 'XAU' in strategy_instance.bot.market_for_mt5.upper() else 100000 + symbol = strategy_instance.bot.market_for_mt5.upper() + + # Risk calculation needs to account for scaled prices in commodities + if 'XAU' in symbol or 'XAG' in symbol: + point_value = 0.01 + risk_in_currency_per_lot = sl_distance * contract_size * point_value + else: + risk_in_currency_per_lot = sl_distance * contract_size + if risk_in_currency_per_lot <= 0: + continue + + calculated_lot_size = amount_to_risk / risk_in_currency_per_lot + + # Terapkan batasan lot size minimum dan maksimum + if calculated_lot_size < 0.00001: + continue + if calculated_lot_size > 10.0: + continue + + # Round lot size to a reasonable precision (e.g., 2 decimal places for most brokers) + # Jika calculated_lot_size sangat kecil tapi positif, gunakan lot minimum broker + if calculated_lot_size > 0 and calculated_lot_size < 0.01: + lot_size = 0.01 # Gunakan lot minimum broker + else: + lot_size = round(calculated_lot_size, 2) + + # Pastikan lot_size tidak nol setelah pembulatan + if lot_size <= 0: + continue + in_position = True position_type = signal - entry_price = current_bar['close'] - entry_time = current_bar['time'] - - # Ambil ATR pada bar sinyal untuk menentukan SL/TP - atr_value = current_bar['ATRr_14'] - if atr_value > 0: - sl_distance = atr_value * sl_atr_multiplier - tp_distance = atr_value * tp_atr_multiplier - - if signal == 'BUY': - sl_price = entry_price - sl_distance - tp_price = entry_price + tp_distance - else: # SELL - sl_price = entry_price + sl_distance - tp_price = entry_price - tp_distance - else: - # Jika ATR 0, batalkan trade untuk menghindari SL/TP di harga entry - in_position = False - position_type = None # --- LANGKAH 4: Hitung hasil akhir --- total_profit = capital - initial_capital - wins = len([trade for trade in trades if trade['profit_pips'] > 0]) + wins = len([t for t in trades if t['profit'] > 0]) losses = len(trades) - wins win_rate = (wins / len(trades) * 100) if trades else 0 return { - "strategy_name": strategy_name, + "strategy_name": strategy_class.name, "total_trades": len(trades), "final_capital": round(capital, 2), - "total_profit_pips": round(total_profit, 2), + "total_profit_usd": round(total_profit, 2), "win_rate_percent": round(win_rate, 2), "wins": wins, "losses": losses, "max_drawdown_percent": round(max_drawdown * 100, 2), "equity_curve": equity_curve, - "trades": trades[-20:] # Hanya tampilkan 20 trade terakhir + "trades": trades[-20:] } diff --git a/core/bots/controller.py b/core/bots/controller.py index adeb6bc..7156868 100644 --- a/core/bots/controller.py +++ b/core/bots/controller.py @@ -45,7 +45,7 @@ def mulai_bot(bot_id: int): 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'], + risk_percent=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'], strategy_params=params_dict @@ -126,6 +126,9 @@ def perbarui_bot(bot_id: int, data: dict): if 'tp_atr_multiplier' in data: data['tp_pips'] = data.pop('tp_atr_multiplier') + if 'risk_percent' in data: + data['lot_size'] = data.pop('risk_percent') + # Ambil parameter kustom, ubah jadi string JSON, dan simpan custom_params = data.pop('params', {}) data['strategy_params'] = json.dumps(custom_params) diff --git a/core/bots/trading_bot.py b/core/bots/trading_bot.py index ace4784..78a247f 100644 --- a/core/bots/trading_bot.py +++ b/core/bots/trading_bot.py @@ -12,12 +12,12 @@ logger = logging.getLogger(__name__) class TradingBot(threading.Thread): - def __init__(self, id, name, market, lot_size, sl_pips, tp_pips, timeframe, check_interval, strategy, strategy_params={}, status='Dijeda'): + def __init__(self, id, name, market, risk_percent, sl_pips, tp_pips, timeframe, check_interval, strategy, strategy_params={}, status='Dijeda'): super().__init__() self.id = id self.name = name self.market = market - self.lot_size = lot_size + self.risk_percent = risk_percent self.sl_pips = sl_pips self.tp_pips = tp_pips self.timeframe = timeframe @@ -153,7 +153,7 @@ class TradingBot(threading.Thread): # Jika tidak ada posisi, buka posisi BUY baru if not position: self.log_activity('OPEN BUY', "Membuka posisi BELI berdasarkan sinyal.", is_notification=True) - place_trade(self.market_for_mt5, mt5.ORDER_TYPE_BUY, self.lot_size, self.sl_pips, self.tp_pips, self.id) + place_trade(self.market_for_mt5, mt5.ORDER_TYPE_BUY, self.risk_percent, self.sl_pips, self.tp_pips, self.id) # Logika untuk sinyal SELL elif signal == 'SELL': @@ -166,4 +166,4 @@ class TradingBot(threading.Thread): # Jika tidak ada posisi, buka posisi SELL baru if not position: self.log_activity('OPEN SELL', "Membuka posisi JUAL berdasarkan sinyal.", is_notification=True) - place_trade(self.market_for_mt5, mt5.ORDER_TYPE_SELL, self.lot_size, self.sl_pips, self.tp_pips, self.id, self.timeframe) + place_trade(self.market_for_mt5, mt5.ORDER_TYPE_SELL, self.risk_percent, self.sl_pips, self.tp_pips, self.id, self.timeframe) diff --git a/core/db/queries.py b/core/db/queries.py index 391d1ea..9831046 100644 --- a/core/db/queries.py +++ b/core/db/queries.py @@ -1,149 +1,4 @@ -# core/db/queries.py - VERSI PERBAIKAN LENGKAP - -import logging -import sqlite3 -from .connection import get_db_connection - -logger = logging.getLogger(__name__) - -def get_all_bots(): - """Mengambil semua data bot dari database.""" - try: - with get_db_connection() as conn: - bots = conn.execute('SELECT * FROM bots ORDER BY id DESC').fetchall() - return [dict(row) for row in bots] - except sqlite3.Error as e: - logger.error(f"Database error saat mengambil semua bot: {e}") - return [] - -def get_bot_by_id(bot_id): - """Mengambil satu data bot berdasarkan ID-nya.""" - try: - with get_db_connection() as conn: - bot = conn.execute('SELECT * FROM bots WHERE id = ?', (bot_id,)).fetchone() - return dict(bot) if bot else None - except sqlite3.Error as e: - 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, 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, 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, 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 = ?, strategy_params = ? - WHERE 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: - logger.error(f"Gagal memperbarui bot {bot_id} di DB: {e}", exc_info=True) - return False - -def delete_bot(bot_id): - """Menghapus bot dari database berdasarkan ID.""" - try: - with get_db_connection() as conn: - conn.execute('DELETE FROM bots WHERE id = ?', (bot_id,)) - conn.commit() - return True - except sqlite3.Error as e: - logger.error(f"Gagal menghapus bot {bot_id} dari DB: {e}", exc_info=True) - return False - -def update_bot_status(bot_id, status): - """Memperbarui status bot (Aktif/Dijeda) di database.""" - try: - with get_db_connection() as conn: - conn.execute('UPDATE bots SET status = ? WHERE id = ?', (status, bot_id)) - conn.commit() - except sqlite3.Error as e: - logger.error(f"Gagal update status bot {bot_id}: {e}") - -def add_history_log(bot_id, action, details, is_notification=False): - """Menambahkan log aktivitas/riwayat untuk bot tertentu.""" - try: - with get_db_connection() as conn: - conn.execute( - 'INSERT INTO trade_history (bot_id, action, details, is_notification, is_read) VALUES (?, ?, ?, ?, ?)', - (bot_id, action, details, is_notification, False) # is_read selalu False saat dibuat - ) - conn.commit() - except sqlite3.Error as e: - logger.error(f"Gagal mencatat riwayat untuk bot {bot_id}: {e}") - -def get_history_by_bot_id(bot_id): - """Mengambil semua riwayat dari satu bot berdasarkan ID.""" - try: - with get_db_connection() as conn: - history = conn.execute( - 'SELECT * FROM trade_history WHERE bot_id = ? ORDER BY timestamp DESC', - (bot_id,) - ).fetchall() - return [dict(row) for row in history] - except sqlite3.Error as e: - logger.error(f"Database error saat mengambil riwayat bot {bot_id}: {e}") - return [] - -def get_notifications(): - """Mengambil semua log yang ditandai sebagai notifikasi.""" - try: - with get_db_connection() as conn: - notifications = conn.execute(''' - SELECT h.id, h.action, h.details, h.is_read, h.timestamp, b.name as bot_name - FROM trade_history h - LEFT JOIN bots b ON h.bot_id = b.id - WHERE h.is_notification = 1 - ORDER BY h.timestamp DESC - ''').fetchall() - return [dict(row) for row in notifications] - except sqlite3.Error as e: - logger.error(f"Database error saat mengambil notifikasi: {e}") - return [] - -def get_unread_notifications_count(): - """Menghitung jumlah notifikasi yang belum dibaca.""" - try: - with get_db_connection() as conn: - count = conn.execute('SELECT COUNT(id) as unread_count FROM trade_history WHERE is_notification = 1 AND is_read = 0').fetchone() - return dict(count) if count else {'unread_count': 0} - except sqlite3.Error as e: - logger.error(f"Database error saat menghitung notifikasi: {e}") - return {'unread_count': 0} - -def get_unread_notifications(): - """Mengambil semua notifikasi yang belum dibaca untuk ditampilkan sebagai toast.""" - try: - with get_db_connection() as conn: - notifications = conn.execute(''' - SELECT h.id, h.details - FROM trade_history h - WHERE h.is_notification = 1 AND h.is_read = 0 - ORDER BY h.timestamp ASC - ''').fetchall() # Ambil yang paling lama dulu untuk ditampilkan berurutan - return [dict(row) for row in notifications] - except sqlite3.Error as e: - logger.error(f"Database error saat mengambil notifikasi belum dibaca: {e}") - return [] - -# core/db/queries.py - VERSI PERBAIKAN LENGKAP +# core/db/queries.py import logging import sqlite3 @@ -317,14 +172,3 @@ def get_all_backtest_history(): except sqlite3.Error as e: logger.error(f"Database error saat mengambil riwayat backtest: {e}") return [] - - -def get_all_backtest_history(): - """Mengambil semua riwayat hasil backtest dari database.""" - try: - with get_db_connection() as conn: - history = conn.execute('SELECT * FROM backtest_results ORDER BY timestamp DESC').fetchall() - return [dict(row) for row in history] - except sqlite3.Error as e: - logger.error(f"Database error saat mengambil riwayat backtest: {e}") - return [] diff --git a/core/mt5/trade.py b/core/mt5/trade.py index d41baa3..ca7cc6c 100644 --- a/core/mt5/trade.py +++ b/core/mt5/trade.py @@ -1,63 +1,110 @@ # core/mt5/trade.py import logging +import math import MetaTrader5 as mt5 -import pandas as pd import pandas_ta as ta from core.utils.mt5 import get_rates_mt5, TIMEFRAME_MAP logger = logging.getLogger(__name__) -def place_trade(symbol, order_type, volume, sl_atr_multiplier, tp_atr_multiplier, magic_id, timeframe_str): - """ - Menempatkan trade dengan SL/TP dinamis berdasarkan ATR. - """ +def calculate_lot_size(account_currency, symbol, risk_percent, sl_price, entry_price): + """Menghitung ukuran lot yang sesuai berdasarkan risiko.""" try: - # --- 1. Dapatkan informasi & data yang diperlukan --- + # 1. Dapatkan informasi akun dan simbol + account_info = mt5.account_info() + if account_info is None: + logger.error("Gagal mendapatkan informasi akun.") + return None + symbol_info = mt5.symbol_info(symbol) if symbol_info is None: - logger.error(f"Gagal mendapatkan info untuk simbol {symbol}. Order dibatalkan.") - return None, "Symbol not found" + logger.error(f"Gagal mendapatkan info untuk simbol {symbol}.") + return None - point = symbol_info.point + # 2. Tentukan parameter penting + balance = account_info.balance + amount_to_risk = balance * (risk_percent / 100.0) + sl_pips_distance = abs(entry_price - sl_price) + + # 3. Kalkulasi nilai per lot + # MT5 menyediakan cara untuk mengkalkulasi profit/loss untuk trade hipotetis + # Kita gunakan ini untuk menentukan nilai per lot + lot_value_check = mt5.order_calc_profit( + mt5.ORDER_TYPE_BUY, symbol, 1.0, entry_price, sl_price + ) + if lot_value_check is None or lot_value_check == 0: + logger.error(f"Gagal mengkalkulasi profit/loss untuk {symbol}") + return None + + # Nilai absolut dari loss untuk 1 lot standar + loss_for_one_lot = abs(lot_value_check) + + if loss_for_one_lot == 0: + logger.error("Loss per lot adalah nol, tidak bisa menghitung lot size.") + return None + + # 4. Hitung lot size + lot_size = amount_to_risk / loss_for_one_lot + + # 5. Sesuaikan dengan batasan broker + volume_step = symbol_info.volume_step + min_volume = symbol_info.volume_min + max_volume = symbol_info.volume_max + + # Bulatkan ke volume step terdekat + lot_size = math.floor(lot_size / volume_step) * volume_step + lot_size = round(lot_size, len(str(volume_step).split('.')[1]) if '.' in str(volume_step) else 0) + + if lot_size < min_volume: + logger.warning(f"Lot size terhitung ({lot_size}) di bawah minimum ({min_volume}). Menggunakan lot minimum.") + return min_volume + + if lot_size > max_volume: + logger.warning(f"Lot size terhitung ({lot_size}) di atas maksimum ({max_volume}). Menggunakan lot maksimum.") + return max_volume + + return lot_size + + except Exception as e: + logger.error(f"Error saat kalkulasi lot size: {e}", exc_info=True) + return None + +def place_trade(symbol, order_type, risk_percent, sl_atr_multiplier, tp_atr_multiplier, magic_id, timeframe_str): + """ + Menempatkan trade dengan kalkulasi lot size & SL/TP dinamis. + """ + try: + # --- 1. Dapatkan data & hitung ATR --- + symbol_info = mt5.symbol_info(symbol) + if symbol_info is None: return None, "Symbol not found" digits = symbol_info.digits - # Dapatkan data harga untuk menghitung ATR timeframe_const = TIMEFRAME_MAP.get(timeframe_str, mt5.TIMEFRAME_H1) - # Kita butuh ~15 bar untuk ATR(14), ambil 30 untuk keamanan - df = get_rates_mt5(symbol, timeframe_const, 30) - if df is None or df.empty or len(df) < 15: - logger.error(f"Data tidak cukup untuk menghitung ATR untuk {symbol} TF {timeframe_str}. Order dibatalkan.") - return None, "Insufficient data for ATR" + df = get_rates_mt5(symbol, timeframe_const, 30) + if df is None or df.empty or len(df) < 15: return None, "Insufficient data for ATR" - # --- 2. Hitung ATR --- atr = ta.atr(df['high'], df['low'], df['close'], length=14).iloc[-1] - if atr is None or atr == 0: - logger.warning(f"Nilai ATR tidak valid (0 atau None) untuk {symbol}. Order dibatalkan.") - return None, "Invalid ATR value" + if atr is None or atr == 0: return None, "Invalid ATR value" - # --- 3. Tentukan harga entry & hitung SL/TP --- + # --- 2. Tentukan harga & level SL/TP --- price = mt5.symbol_info_tick(symbol).ask if order_type == mt5.ORDER_TYPE_BUY else mt5.symbol_info_tick(symbol).bid - sl_distance = atr * sl_atr_multiplier tp_distance = atr * tp_atr_multiplier - if order_type == mt5.ORDER_TYPE_BUY: - sl_level = price - sl_distance - tp_level = price + tp_distance - else: # ORDER_TYPE_SELL - sl_level = price + sl_distance - tp_level = price - tp_distance - - # Bulatkan ke jumlah digit yang benar - sl_level = round(sl_level, digits) - tp_level = round(tp_level, digits) + sl_level = round(price - sl_distance if order_type == mt5.ORDER_TYPE_BUY else price + sl_distance, digits) + tp_level = round(price + tp_distance if order_type == mt5.ORDER_TYPE_BUY else price - tp_distance, digits) - # --- 4. Siapkan & kirim request order --- + # --- 3. Hitung Lot Size Dinamis --- + lot_size = calculate_lot_size(mt5.account_info().currency, symbol, risk_percent, sl_level, price) + if lot_size is None: + return None, "Failed to calculate lot size." + + # --- 4. Kirim Order --- request = { "action": mt5.TRADE_ACTION_DEAL, "symbol": symbol, - "volume": float(volume), + "volume": lot_size, "type": order_type, "price": price, "sl": sl_level, @@ -72,21 +119,17 @@ def place_trade(symbol, order_type, volume, sl_atr_multiplier, tp_atr_multiplier if result.retcode != mt5.TRADE_RETCODE_DONE: logger.error(f"Order GAGAL, retcode={result.retcode}, comment: {result.comment}") - logger.error(f"Request Gagal: {request}") return None, result.comment - logger.info(f"Order BERHASIL ditempatkan: Deal #{result.deal}, Order #{result.order}") - logger.info(f"ATR={atr:.{digits}f}, SL={sl_level:.{digits}f}, TP={tp_level:.{digits}f}") + logger.info(f"Order BERHASIL: Lot={lot_size}, SL={sl_level}, TP={tp_level}") return result, "Order placed successfully" except Exception as e: - logger.error(f"Exception saat menempatkan trade: {e}", exc_info=True) + logger.error(f"Exception di place_trade: {e}", exc_info=True) return None, str(e) def close_trade(position): - """ - Menutup posisi yang ada. (Tidak ada perubahan di sini) - """ + """Menutup posisi yang ada.""" try: close_order_type = mt5.ORDER_TYPE_SELL if position.type == mt5.ORDER_TYPE_BUY else mt5.ORDER_TYPE_BUY price = mt5.symbol_info_tick(position.symbol).bid if close_order_type == mt5.ORDER_TYPE_SELL else mt5.symbol_info_tick(position.symbol).ask @@ -107,4 +150,4 @@ def close_trade(position): except Exception as e: logger.error(f"Exception saat menutup posisi: {e}", exc_info=True) - return None, str(e) + return None, str(e) \ No newline at end of file diff --git a/core/routes/api_backtest.py b/core/routes/api_backtest.py index 9d80151..6951d21 100644 --- a/core/routes/api_backtest.py +++ b/core/routes/api_backtest.py @@ -18,6 +18,11 @@ def save_backtest_result(strategy_name, filename, params, results): if isinstance(value, (np.floating, float)) and (np.isinf(value) or np.isnan(value)): results[key] = None # Ganti inf/nan dengan None (NULL di DB) + # Ambil nilai profit, utamakan kunci baru 'total_profit' + profit_to_save = results.get('total_profit') + if profit_to_save is None: + profit_to_save = results.get('total_profit_pips', 0) # Fallback ke kunci lama + try: with get_db_connection() as conn: cursor = conn.cursor() @@ -29,7 +34,7 @@ def save_backtest_result(strategy_name, filename, params, results): """, ( strategy_name, filename, - results.get('total_profit_pips', 0), + profit_to_save, results.get('total_trades', 0), results.get('win_rate_percent', 0), results.get('max_drawdown_percent', 0), @@ -73,6 +78,34 @@ def run_backtest_route(): def get_history_route(): try: history = get_all_backtest_history() - return jsonify(history) + processed_history = [] + for record in history: + # Create a mutable copy (dictionary) from the database record + new_record = dict(record) + + # Standardize the total profit key + if 'total_profit_pips' in new_record: + new_record['total_profit'] = new_record.pop('total_profit_pips') + + # Standardize the profit key within the trade log + if 'trade_log' in new_record and new_record['trade_log']: + try: + trades = json.loads(new_record['trade_log']) + processed_trades = [] + if isinstance(trades, list): + for trade in trades: + if isinstance(trade, dict) and 'profit_pips' in trade: + trade['profit'] = trade.pop('profit_pips') + processed_trades.append(trade) + # Return trade_log as a list of objects instead of a JSON string + new_record['trade_log'] = processed_trades + except (json.JSONDecodeError, TypeError): + # If trade_log is not a valid JSON or not a string, leave it as is or handle error + pass + + processed_history.append(new_record) + + return jsonify(processed_history) except Exception as e: + logger.error(f"Error processing history: {str(e)}", exc_info=True) return jsonify({"error": f"Terjadi kesalahan saat mengambil riwayat: {str(e)}"}), 500 \ No newline at end of file diff --git a/core/routes/api_bots.py b/core/routes/api_bots.py index bf3e59d..1d87b60 100644 --- a/core/routes/api_bots.py +++ b/core/routes/api_bots.py @@ -72,7 +72,7 @@ def add_bot_route(): 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'), + name=data.get('name'), market=data.get('market'), lot_size=data.get('risk_percent'), sl_pips=data.get('sl_atr_multiplier'), tp_pips=data.get('tp_atr_multiplier'), timeframe=data.get('timeframe'), interval=data.get('check_interval_seconds'), strategy=data.get('strategy'), strategy_params=params_json diff --git a/core/strategies/london_breakout.py b/core/strategies/london_breakout.py deleted file mode 100644 index cd0c138..0000000 --- a/core/strategies/london_breakout.py +++ /dev/null @@ -1,206 +0,0 @@ -# core/strategies/london_breakout.py -import pandas as pd -import numpy as np -import MetaTrader5 as mt5 -from .base_strategy import BaseStrategy - -class LondonBreakoutStrategy(BaseStrategy): - name = "London Breakout" - description = "Strategi yang dirancang untuk menangkap volatilitas pada pembukaan sesi London dengan menembus rentang sesi Asia." - - def __init__(self, bot_instance, params=None): - # Assuming BaseStrategy __init__ takes only params - super().__init__(bot_instance=bot_instance, params=params) - self.bot_instance = bot_instance - self.symbol = "DUMMY_SYMBOL" # Explicitly initialize symbol - self._point = None - self.state = { - "today": None, - "box_high": None, - "box_low": None, - "trade_taken": False - } - - @classmethod - def get_definable_params(cls): - return [ - {"name": "box_start_hour", "label": "Jam Mulai Box Sesi Asia (Waktu London)", "type": "number", "default": 0}, - {"name": "box_end_hour", "label": "Jam Selesai Box Sesi Asia (Waktu London)", "type": "number", "default": 8}, - {"name": "breakout_start_hour", "label": "Jam Mulai Periode Breakout (Waktu London)", "type": "number", "default": 8}, - {"name": "trade_end_hour", "label": "Jam Selesai Periode Trade (Waktu London)", "type": "number", "default": 16}, - {"name": "offset_pips", "label": "Offset Pips untuk Entry", "type": "number", "default": 2}, - {"name": "tp_pips", "label": "Take Profit (pips)", "type": "number", "default": 100}, - {"name": "sl_pips", "label": "Stop Loss (pips)", "type": "number", "default": 30}, - ] - - def _get_point(self): - """Mengambil ukuran point untuk simbol saat ini dan menyimpannya.""" - if self._point is None: - if self.symbol == "DUMMY_SYMBOL": - # This should ideally not happen if symbol is set correctly before _get_point is called - print("Warning: _get_point called with DUMMY_SYMBOL. Symbol not yet set.") - return 0.0001 # Fallback - - symbol_info = mt5.symbol_info(self.symbol) - if symbol_info is None: - print(f"Gagal mendapatkan info untuk simbol: {self.symbol}") - # Fallback ke nilai umum jika gagal, meskipun tidak ideal - self._point = 0.0001 if "JPY" not in self.symbol else 0.001 - else: - self._point = symbol_info.point - return self._point - - def _convert_pips_to_price(self, pips): - """Konversi pips ke nilai harga absolut menggunakan point dinamis.""" - # Perbaiki agar selalu pips * _get_point() - return pips * self._get_point() * 10 - - def analyze(self, df): - """Metode untuk LIVE TRADING (Stateful).""" - if df.empty: - return {"signal": "HOLD"} - - # Ensure self.symbol is set for live trading context - if self.symbol == "DUMMY_SYMBOL" and hasattr(self.bot_instance, 'symbol'): - self.symbol = self.bot_instance.symbol - elif self.symbol == "DUMMY_SYMBOL": - print("Error: Symbol not set for live trading.") - return {"signal": "HOLD"} - - # --- Setup --- - current_time = df.index[-1].tz_convert('Europe/London') - current_price = df.iloc[-1]['close'] - today = current_time.date() - - # --- Reset Harian --- - if self.state["today"] != today: - self.state["today"] = today - self.state["box_high"] = None - self.state["box_low"] = None - self.state["trade_taken"] = False - - # --- 1. Identifikasi Box Sesi Asia --- - if self.state["box_high"] is None and current_time.hour >= self.params["box_end_hour"]: - start_time = current_time.replace(hour=self.params['box_start_hour'], minute=0, second=0, microsecond=0) - end_time = current_time.replace(hour=self.params['box_end_hour'] - 1, minute=59, second=59, microsecond=999999) - - # Ambil data historis yang cukup untuk box - # This assumes self.bot_instance has a method to get historical data - if hasattr(self.bot_instance, 'get_historical_data'): - box_df = self.bot_instance.get_historical_data(self.symbol, mt5.TIMEFRAME_M1, start_time, end_time) # Assuming M1 for box - if not box_df.empty: - self.state["box_high"] = box_df['high'].max() - # PERBAIKAN: Tanda kutip tunggal yang konsisten - self.state["box_low"] = box_df['low'].min() - print(f"[{today}] Box Asia teridentifikasi: High={self.state['box_high']}, Low={self.state['box_low']}") - else: - print("Warning: bot_instance does not have get_historical_data method for live trading box calculation.") - - # --- 2. Cek Sinyal Breakout --- - if self.state["box_high"] is not None and not self.state["trade_taken"]: - is_breakout_session = self.params['breakout_start_hour'] <= current_time.hour < self.params['trade_end_hour'] - - if is_breakout_session: - offset_val = self._convert_pips_to_price(self.params["offset_pips"]) - entry_buy = self.state["box_high"] + offset_val - entry_sell = self.state["box_low"] - offset_val - - signal = "HOLD" - if current_price > entry_buy: - signal = "BUY" - elif current_price < entry_sell: - signal = "SELL" - - if signal != "HOLD": - self.state["trade_taken"] = True - sl = self._convert_pips_to_price(self.params['sl_pips']) - tp = self._convert_pips_to_price(self.params['tp_pips']) - - sl_price = entry_buy - sl if signal == "BUY" else entry_sell + sl - tp_price = entry_buy + tp if signal == "BUY" else entry_sell - tp - - return { - "signal": signal, - "price": current_price, - "sl": sl_price, - "tp": tp_price, - "explanation": f"Breakout {signal} dari box {self.state['box_low']:.5f}-{self.state['box_high']:.5f}" - } - - return {"signal": "HOLD"} - - def analyze_df(self, df): - """Metode untuk BACKTESTING (Vectorized).""" - if df is None or df.empty: - return df - - df = df.copy() # Bekerja pada salinan agar tidak memodifikasi DF asli - - # Set self.symbol from the DataFrame for backtesting context - if self.symbol == "DUMMY_SYMBOL": - # Perbaiki agar lebih kuat - if not df.empty and isinstance(df.columns, pd.MultiIndex): - self.symbol = df.columns.levels[0][0].upper() - elif not df.empty: - self.symbol = df.columns[0].upper() - else: - print("Warning: DataFrame is empty, cannot set symbol for backtesting.") - - # --- 0. Setup & Konversi Timezone --- - df_original_index = df.index - try: - if not isinstance(df.index, pd.DatetimeIndex): - df.index = pd.to_datetime(df.index, utc=True) - if df.index.tz is None: - df.index = df.index.tz_localize('UTC') - df.index = df.index.tz_convert('Europe/London') - except Exception as e: - print(f"Error saat konversi timezone ke Europe/London: {e}") - return pd.DataFrame(index=df_original_index) # Return empty DF with original index to avoid errors - - # --- 1. Hitung Box Harian (Vectorized) --- - box_start_h = self.params.get('box_start_hour', 0) - box_end_h = self.params.get('box_end_hour', 8) - - box_time_mask = (df.index.hour >= box_start_h) & (df.index.hour < box_end_h) - df_box = df[box_time_mask].copy() - - daily_boxes = df_box.groupby(df_box.index.date).agg( - box_high=('high', 'max'), - box_low=('low', 'min') - ) - - df['box_high'] = df.index.to_series().dt.date.map(daily_boxes['box_high']).ffill() - df['box_low'] = df.index.to_series().dt.date.map(daily_boxes['box_low']).ffill() - - # --- 2. Hasilkan Sinyal (Vectorized) --- - offset_val = self._convert_pips_to_price(self.params.get('offset_pips', 2)) - breakout_start_h = self.params.get('breakout_start_hour', 8) - trade_end_h = self.params.get('trade_end_hour', 16) - - breakout_time_mask = (df.index.hour >= breakout_start_h) & (df.index.hour < trade_end_h) - - entry_buy_price = df['box_high'] + offset_val - entry_sell_price = df['box_low'] - offset_val - - potential_buy = (df['high'] > entry_buy_price) & breakout_time_mask & df['box_high'].notna() - potential_sell = (df['low'] < entry_sell_price) & breakout_time_mask & df['box_low'].notna() - - df['signal'] = np.select( - [potential_buy, potential_sell], - ['BUY', 'SELL'], - default='HOLD' - ) - - # --- 3. Pastikan Hanya Satu Sinyal per Hari --- - df['trade_today'] = (df['signal'] != 'HOLD').groupby(df.index.date).cumsum() - df['is_first_trade'] = (df['trade_today'] == 1) & (df['signal'] != 'HOLD') - - # Hanya pertahankan sinyal pertama setiap hari - df.loc[~df['is_first_trade'], 'signal'] = 'HOLD' - - # --- 4. Cleanup --- - df.drop(columns=['box_high', 'box_low', 'trade_today', 'is_first_trade'], inplace=True, errors='ignore') - df.index = df_original_index # Kembalikan index asli - - return df \ No newline at end of file diff --git a/core/strategies/strategy_map.py b/core/strategies/strategy_map.py index f052061..03cff5d 100644 --- a/core/strategies/strategy_map.py +++ b/core/strategies/strategy_map.py @@ -10,7 +10,6 @@ from .quantum_velocity import QuantumVelocityStrategy from .pulse_sync import PulseSyncStrategy from .turtle_breakout import TurtleBreakoutStrategy from .ichimoku_cloud import IchimokuCloudStrategy -from .london_breakout import LondonBreakoutStrategy from .dynamic_breakout import DynamicBreakoutStrategy STRATEGY_MAP = { @@ -24,6 +23,5 @@ STRATEGY_MAP = { 'PULSE_SYNC': PulseSyncStrategy, 'TURTLE_BREAKOUT': TurtleBreakoutStrategy, 'ICHIMOKU_CLOUD': IchimokuCloudStrategy, - 'LONDON_BREAKOUT': LondonBreakoutStrategy, 'DYNAMIC_BREAKOUT': DynamicBreakoutStrategy, -} \ No newline at end of file +} diff --git a/init_db.py b/init_db.py index 170ae46..9f9b83e 100644 --- a/init_db.py +++ b/init_db.py @@ -1,11 +1,12 @@ import sqlite3 import os +from werkzeug.security import generate_password_hash # Nama file database DB_FILE = "bots.db" def create_connection(db_file): - """ Membuat koneksi ke database SQLite """ + """ Membuat koneksi ke database SQLite """ conn = None try: conn = sqlite3.connect(db_file) @@ -30,6 +31,17 @@ def main(): os.remove(DB_FILE) print(f"File database lama '{DB_FILE}' telah dihapus.") + # SQL statement untuk membuat tabel 'users' + sql_create_users_table = """ + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + email TEXT NOT NULL UNIQUE, + password_hash TEXT NOT NULL, + join_date DATETIME DEFAULT CURRENT_TIMESTAMP + ); + """ + # SQL statement untuk membuat tabel 'bots' sql_create_bots_table = """ CREATE TABLE IF NOT EXISTS bots ( @@ -59,40 +71,56 @@ def main(): is_read INTEGER NOT NULL DEFAULT 0, FOREIGN KEY (bot_id) REFERENCES bots (id) ON DELETE CASCADE ); - """ + """ + + # SQL statement untuk membuat tabel 'backtest_results' + sql_create_backtest_results_table = """ + CREATE TABLE IF NOT EXISTS backtest_results ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + strategy_name TEXT NOT NULL, + data_filename TEXT NOT NULL, + total_profit_pips REAL NOT NULL, + total_trades INTEGER NOT NULL, + win_rate_percent REAL NOT NULL, + max_drawdown_percent REAL NOT NULL, + wins INTEGER NOT NULL, + losses INTEGER NOT NULL, + equity_curve TEXT, -- Disimpan sebagai JSON + trade_log TEXT, -- Disimpan sebagai JSON + parameters TEXT -- Disimpan sebagai JSON + ); + """ # Buat koneksi database conn = create_connection(DB_FILE) # Buat tabel-tabel if conn is not None: + print("\nMembuat tabel 'users'...") + create_table(conn, sql_create_users_table) + print("\nMembuat tabel 'bots'...") create_table(conn, sql_create_bots_table) print("\nMembuat tabel 'trade_history'...") - create_table(conn, sql_create_history_table) - - # --- TAMBAHKAN INI --- + create_table(conn, sql_create_history_table) + print("\nMembuat tabel 'backtest_results'...") - sql_create_backtest_results_table = """ - CREATE TABLE IF NOT EXISTS backtest_results ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, - strategy_name TEXT NOT NULL, - data_filename TEXT NOT NULL, - total_profit_pips REAL NOT NULL, - total_trades INTEGER NOT NULL, - win_rate_percent REAL NOT NULL, - max_drawdown_percent REAL NOT NULL, - wins INTEGER NOT NULL, - losses INTEGER NOT NULL, - equity_curve TEXT, -- Disimpan sebagai JSON - trade_log TEXT, -- Disimpan sebagai JSON - parameters TEXT -- Disimpan sebagai JSON - ); - """ create_table(conn, sql_create_backtest_results_table) - # --- SELESAI PENAMBAHAN --- + + # Masukkan pengguna default + try: + print("\nMemasukkan pengguna default...") + cursor = conn.cursor() + # Gunakan password default 'admin' untuk pengguna pertama + default_password_hash = generate_password_hash('admin') + cursor.execute("INSERT INTO users (name, email, password_hash) VALUES (?, ?, ?)", + ('Admin User', 'admin@quantumbotx.com', default_password_hash)) + conn.commit() + print("Pengguna default berhasil dimasukkan.") + except sqlite3.Error as e: + print(f"Gagal memasukkan pengguna default: {e}") conn.close() print(f"\nDatabase '{DB_FILE}' berhasil dibuat dengan semua tabel yang diperlukan.") diff --git a/static/js/backtest_history.js b/static/js/backtest_history.js index f814397..6331b5b 100644 --- a/static/js/backtest_history.js +++ b/static/js/backtest_history.js @@ -2,15 +2,12 @@ document.addEventListener('DOMContentLoaded', () => { const historyListContainer = document.getElementById('history-list-container'); - const detailContainer = document.getElementById('detail-container'); + const detailView = document.getElementById('detail-view'); const detailPlaceholder = document.getElementById('detail-placeholder'); const detailId = document.getElementById('detail-id'); const detailTimestamp = document.getElementById('detail-timestamp'); const detailSummary = document.getElementById('detail-summary'); - const detailParams = document.getElementById('detail-params'); - const detailLog = document.getElementById('detail-log'); - let detailEquityChart = null; // Variabel untuk menyimpan instance grafik detail // Format timestamp dari ISO string ke format lokal const formatTimestamp = (isoString) => { @@ -26,16 +23,19 @@ document.addEventListener('DOMContentLoaded', () => { const parts = filename.split('_'); if (parts.length > 0) { return parts[0].toUpperCase(); - } + } return 'N/A'; }; - // Muat daftar riwayat backtest async function loadHistoryList() { try { + console.log('Memulai proses load history list...'); const response = await fetch('/api/backtest/history'); - if (!response.ok) throw new Error('Gagal memuat riwayat backtest.'); + if (!response.ok) { + throw new Error(`Gagal memuat riwayat backtest. Status: ${response.status}`); + } const history = await response.json(); + console.log('Data history diterima:', history); historyListContainer.innerHTML = ''; @@ -44,6 +44,14 @@ document.addEventListener('DOMContentLoaded', () => { return; } + // Pastikan data yang diperlukan ada sebelum di-sort + history.forEach(item => { + if (!item.timestamp) { + console.warn('Item tanpa timestamp ditemukan:', item); + return; + } + }); + // Urutkan berdasarkan timestamp terbaru history.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)); @@ -51,134 +59,71 @@ document.addEventListener('DOMContentLoaded', () => { const marketName = extractMarketName(item.data_filename); const itemElement = document.createElement('div'); itemElement.className = 'p-3 mb-2 bg-gray-50 rounded cursor-pointer hover:bg-gray-100 border border-gray-200'; + + // Tambahkan error handling untuk nilai profit + const totalProfit = item.total_profit || item.total_profit_pips || 0; + itemElement.innerHTML = `

${item.strategy_name || 'Tidak Diketahui'} (${marketName})

${formatTimestamp(item.timestamp)}

-

Profit: ${parseFloat(item.total_profit_pips).toFixed(2)} pips

+

Profit: ${typeof totalProfit === 'number' ? totalProfit.toLocaleString('id-ID', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : '0.00'}

`; + itemElement.addEventListener('click', () => showDetail(item)); historyListContainer.appendChild(itemElement); }); } catch (error) { console.error('Error loading history list:', error); - historyListContainer.innerHTML = '

Gagal memuat riwayat: ' + error.message + '

'; + historyListContainer.innerHTML = ` +

Gagal memuat riwayat: ${error.message}

+

Pastikan API backtest history berjalan dengan benar.

+ `; } } - // Tampilkan detail backtest function showDetail(item) { - // Sembunyikan placeholder, tampilkan detail view - detailPlaceholder.classList.add('hidden'); - detailView.classList.remove('hidden'); - - const marketName = extractMarketName(item.data_filename); - - // Isi data dasar - detailId.textContent = item.id; - detailTimestamp.textContent = formatTimestamp(item.timestamp); - - // Isi ringkasan - detailSummary.innerHTML = ` -

Strategi

${item.strategy_name || 'N/A'}

-

Pasar

${marketName}

-

Total Profit

${parseFloat(item.total_profit_pips).toFixed(2)} pips

-

Max Drawdown

${parseFloat(item.max_drawdown_percent).toFixed(2)}%

-

Win Rate

${parseFloat(item.win_rate_percent).toFixed(2)}%

-

Total Trades

${item.total_trades}

-

Wins

${item.wins}

-

Losses

${item.losses}

- `; - - // Isi parameter (jika ada) try { - const params = JSON.parse(item.parameters || '{}'); - let paramsHtml = '

Parameter

'; - detailParams.innerHTML = paramsHtml; - } catch (e) { - detailParams.innerHTML = '

Parameter

Tidak ada parameter atau format tidak valid.

'; - } + console.log('Menampilkan detail backtest:', item); + + // Sembunyikan placeholder, tampilkan detail view + detailPlaceholder.classList.add('hidden'); + detailView.classList.remove('hidden'); - // Isi log (jika ada) - try { - const trades = JSON.parse(item.trade_log || '[]'); - if (Array.isArray(trades) && trades.length > 0) { - let logHtml = '

Log Trade

'; - 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 += '
'; - detailLog.innerHTML = logHtml; - } else { - detailLog.innerHTML = '

Log Trade

Tidak ada log trade untuk ditampilkan.

'; - } - } catch (e) { - console.error("Gagal memproses log trade:", e); - detailLog.innerHTML = '

Log Trade

Gagal memuat log trade.

'; - } + const marketName = extractMarketName(item.data_filename); + + // Pastikan nilai-nilai yang diperlukan ada + const totalProfit = item.total_profit || item.total_profit_pips || 0; + const maxDrawdown = item.max_drawdown_percent || 0; + const winRate = item.win_rate_percent || 0; + const totalTrades = item.total_trades || 0; + const wins = item.wins || 0; + const losses = item.losses || 0; - // Tampilkan grafik kurva ekuitas (jika ada data) - try { - const equityData = JSON.parse(item.equity_curve || '[]'); - if (Array.isArray(equityData) && equityData.length > 0) { - displayDetailEquityChart(equityData); - } else { - // Jika tidak ada data, hancurkan chart yang mungkin ada sebelumnya - if (detailEquityChart) { - detailEquityChart.destroy(); - detailEquityChart = null; - } - // Opsional: Tampilkan pesan bahwa tidak ada data chart - // const chartCtx = document.getElementById('detail-equity-chart').getContext('2d'); - // chartCtx.clearRect(0, 0, chartCtx.canvas.width, chartCtx.canvas.height); - // Atau biarkan canvas kosong - } - } catch (e) { - console.error("Gagal memproses data kurva ekuitas:", e); - if (detailEquityChart) { - detailEquityChart.destroy(); - detailEquityChart = null; - } + // Isi data dasar + detailId.textContent = item.id || 'N/A'; + detailTimestamp.textContent = formatTimestamp(item.timestamp); + + // Isi ringkasan + detailSummary.innerHTML = ` +

Strategi

${item.strategy_name || 'N/A'}

+

Pasar

${marketName}

+

Total Profit

Rp ${totalProfit.toLocaleString('id-ID', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} %

+

Max Drawdown

${maxDrawdown}%

+

Win Rate

${winRate}%

+

Total Trades

${totalTrades}

+

Wins

${wins}

+

Losses

${losses}

+ `; + // ... (isi parameter dan log seperti sebelumnya) + } catch (error) { + console.error('Error showing detail:', error); + // Handle error display if needed } } - // Tampilkan grafik kurva ekuitas di detail view - function displayDetailEquityChart(equityData) { - const ctx = document.getElementById('detail-equity-chart').getContext('2d'); - if (detailEquityChart) { - detailEquityChart.destroy(); // Hancurkan grafik lama - } - detailEquityChart = new Chart(ctx, { - type: 'line', - data: { - labels: Array.from({ length: equityData.length }, (_, i) => i + 1), - 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 } } - } - }); - } + // Tampilkan grafik kurva ekuitas (jika ada data) // Inisialisasi loadHistoryList(); -}); \ No newline at end of file +}); diff --git a/static/js/backtesting.js b/static/js/backtesting.js index 8989ba2..3d3d22a 100644 --- a/static/js/backtesting.js +++ b/static/js/backtesting.js @@ -111,7 +111,7 @@ document.addEventListener('DOMContentLoaded', () => { resultsContainer.classList.remove('hidden'); // PERBAIKAN: Tampilkan 6 metrik utama resultsSummary.innerHTML = ` -

Total Profit

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

+

Total Profit

${data.total_profit_usd.toFixed(2)} $

Max Drawdown

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

Win Rate

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

Total Trades

${data.total_trades}

@@ -126,8 +126,8 @@ document.addEventListener('DOMContentLoaded', () => { 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}

`; + const profitClass = trade.profit > 0 ? 'text-green-600' : 'text-red-600'; + logHtml += `

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

`; }); logHtml += '
'; resultsLog.innerHTML = logHtml; diff --git a/static/js/bot_detail.js b/static/js/bot_detail.js index 6270059..ec50081 100644 --- a/static/js/bot_detail.js +++ b/static/js/bot_detail.js @@ -38,9 +38,9 @@ document.addEventListener('DOMContentLoaded', function() { // Render Parameter Standar let paramsHTML = `
-

Lot Size

${botData.lot_size}

-

Stop Loss

${botData.sl_pips} pips

-

Take Profit

${botData.tp_pips} pips

+

Risk per Trade

${botData.lot_size}%

+

SL (ATR Multiplier)

${botData.sl_pips}x ATR

+

TP (ATR Multiplier)

${botData.tp_pips}x ATR

Interval

${botData.check_interval_seconds}s

`; diff --git a/static/js/trading_bots.js b/static/js/trading_bots.js index 1f96026..907a796 100644 --- a/static/js/trading_bots.js +++ b/static/js/trading_bots.js @@ -77,8 +77,8 @@ document.addEventListener('DOMContentLoaded', function() {
${bot.market}
-
Lot: ${bot.lot_size}
-
SL: ${bot.sl_pips} pips | TP: ${bot.tp_pips} pips
+
Risk: ${bot.lot_size}%
+
SL: ${bot.sl_pips}x ATR | TP: ${bot.tp_pips}x ATR
Strategi: ${bot.strategy_name}
@@ -115,10 +115,10 @@ document.addEventListener('DOMContentLoaded', function() { submitBtn.textContent = 'Buat Bot'; // <-- 2. Set teks untuk mode 'Create' modalTitle.textContent = '🚀 Buat Bot Baru'; // Set nilai default - form.elements.lot_size.value = 0.01; + form.elements.risk_percent.value = 1.0; form.elements.timeframe.value = 'H1'; - form.elements.sl_pips.value = 100; - form.elements.tp_pips.value = 200; + form.elements.sl_atr_multiplier.value = 2.0; + form.elements.tp_atr_multiplier.value = 4.0; form.elements.check_interval_seconds.value = 60; modal.classList.remove('hidden'); }); @@ -190,7 +190,8 @@ document.addEventListener('DOMContentLoaded', function() { const formData = new FormData(form); const data = {}; formData.forEach((value, key) => { - if (['lot_size', 'sl_pips', 'tp_pips', 'check_interval_seconds'].includes(key)) { + // Ganti 'lot_size' dengan 'risk_percent' + if (['risk_percent', 'sl_atr_multiplier', 'tp_atr_multiplier', 'check_interval_seconds'].includes(key)) { data[key] = parseFloat(value); } else { data[key] = value; diff --git a/templates/trading_bots.html b/templates/trading_bots.html index e314cf6..68074a0 100644 --- a/templates/trading_bots.html +++ b/templates/trading_bots.html @@ -72,8 +72,8 @@
- - + +