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 @@
- - + +