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.
This commit is contained in:
Reynov Christian
2025-08-23 12:11:32 +08:00
parent 4f279b21fe
commit 34c324d52e
15 changed files with 362 additions and 643 deletions
+106 -76
View File
@@ -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:]
}
+4 -1
View File
@@ -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)
+4 -4
View File
@@ -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)
+1 -157
View File
@@ -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 []
+84 -41
View File
@@ -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)
+35 -2
View File
@@ -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
+1 -1
View File
@@ -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
-206
View File
@@ -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
+1 -3
View File
@@ -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,
}
}
+51 -23
View File
@@ -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.")
+60 -115
View File
@@ -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 = `
<p class="font-medium text-gray-800">${item.strategy_name || 'Tidak Diketahui'} (${marketName})</p>
<p class="text-xs text-gray-500">${formatTimestamp(item.timestamp)}</p>
<p class="text-sm mt-1"><span class="font-semibold">Profit:</span> ${parseFloat(item.total_profit_pips).toFixed(2)} pips</p>
<p class="text-sm mt-1"><span class="font-semibold">Profit:</span> ${typeof totalProfit === 'number' ? totalProfit.toLocaleString('id-ID', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : '0.00'}</p>
`;
itemElement.addEventListener('click', () => showDetail(item));
historyListContainer.appendChild(itemElement);
});
} catch (error) {
console.error('Error loading history list:', error);
historyListContainer.innerHTML = '<p class="text-red-500 text-center py-4">Gagal memuat riwayat: ' + error.message + '</p>';
historyListContainer.innerHTML = `
<p class="text-red-500 text-center py-4">Gagal memuat riwayat: ${error.message}</p>
<p class="text-gray-500 text-center mt-2">Pastikan API backtest history berjalan dengan benar.</p>
`;
}
}
// 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 = `
<div class="p-3 bg-gray-50 rounded"><p class="text-xs text-gray-500">Strategi</p><p class="font-bold">${item.strategy_name || 'N/A'}</p></div>
<div class="p-3 bg-gray-50 rounded"><p class="text-xs text-gray-500">Pasar</p><p class="font-bold">${marketName}</p></div>
<div class="p-3 bg-gray-50 rounded"><p class="text-xs text-gray-500">Total Profit</p><p class="font-bold">${parseFloat(item.total_profit_pips).toFixed(2)} pips</p></div>
<div class="p-3 bg-gray-50 rounded"><p class="text-xs text-gray-500">Max Drawdown</p><p class="font-bold">${parseFloat(item.max_drawdown_percent).toFixed(2)}%</p></div>
<div class="p-3 bg-gray-50 rounded"><p class="text-xs text-gray-500">Win Rate</p><p class="font-bold">${parseFloat(item.win_rate_percent).toFixed(2)}%</p></div>
<div class="p-3 bg-gray-50 rounded"><p class="text-xs text-gray-500">Total Trades</p><p class="font-bold">${item.total_trades}</p></div>
<div class="p-3 bg-gray-50 rounded"><p class="text-xs text-gray-500">Wins</p><p class="font-bold">${item.wins}</p></div>
<div class="p-3 bg-gray-50 rounded"><p class="text-xs text-gray-500">Losses</p><p class="font-bold">${item.losses}</p></div>
`;
// Isi parameter (jika ada)
try {
const params = JSON.parse(item.parameters || '{}');
let paramsHtml = '<h4 class="font-semibold mb-2">Parameter</h4><ul class="list-disc pl-5 text-sm">';
for (const [key, value] of Object.entries(params)) {
paramsHtml += `<li><span class="font-medium">${key}:</span> ${value}</li>`;
}
paramsHtml += '</ul>';
detailParams.innerHTML = paramsHtml;
} catch (e) {
detailParams.innerHTML = '<h4 class="font-semibold mb-2">Parameter</h4><p class="text-gray-500">Tidak ada parameter atau format tidak valid.</p>';
}
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 = '<h4 class="font-semibold mt-4 mb-2">Log Trade</h4><div class="text-xs font-mono border rounded p-3 bg-gray-50 max-h-40 overflow-y-auto">';
trades.forEach(trade => {
const profitClass = trade.profit_pips > 0 ? 'text-green-600' : 'text-red-600';
logHtml += `<p>Entry: ${trade.entry.toFixed(4)} | Exit: ${trade.exit.toFixed(4)} | Profit: <span class="${profitClass}">${trade.profit_pips.toFixed(2)} pips</span> | Reason: ${trade.reason}</p>`;
});
logHtml += '</div>';
detailLog.innerHTML = logHtml;
} else {
detailLog.innerHTML = '<h4 class="font-semibold mt-4 mb-2">Log Trade</h4><p class="text-gray-500">Tidak ada log trade untuk ditampilkan.</p>';
}
} catch (e) {
console.error("Gagal memproses log trade:", e);
detailLog.innerHTML = '<h4 class="font-semibold mt-4 mb-2">Log Trade</h4><p class="text-red-500">Gagal memuat log trade.</p>';
}
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 = `
<div class="p-3 bg-gray-50 rounded"><p class="text-xs text-gray-500">Strategi</p><p class="font-bold">${item.strategy_name || 'N/A'}</p></div>
<div class="p-3 bg-gray-50 rounded"><p class="text-xs text-gray-500">Pasar</p><p class="font-bold">${marketName}</p></div>
<div class="p-3 bg-gray-50 rounded"><p class="text-xs text-gray-500">Total Profit</p><p class="font-bold">Rp ${totalProfit.toLocaleString('id-ID', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} %</p></div>
<div class="p-3 bg-gray-50 rounded"><p class="text-xs text-gray-500">Max Drawdown</p><p class="font-bold">${maxDrawdown}%</p></div>
<div class="p-3 bg-gray-50 rounded"><p class="text-xs text-gray-500">Win Rate</p><p class="font-bold">${winRate}%</p></div>
<div class="p-3 bg-gray-50 rounded"><p class="text-xs text-gray-500">Total Trades</p><p class="font-bold">${totalTrades}</p></div>
<div class="p-3 bg-gray-50 rounded"><p class="text-xs text-gray-500">Wins</p><p class="font-bold">${wins}</p></div>
<div class="p-3 bg-gray-50 rounded"><p class="text-xs text-gray-500">Losses</p><p class="font-bold">${losses}</p></div>
`;
// ... (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();
});
});
+3 -3
View File
@@ -111,7 +111,7 @@ document.addEventListener('DOMContentLoaded', () => {
resultsContainer.classList.remove('hidden');
// PERBAIKAN: Tampilkan 6 metrik utama
resultsSummary.innerHTML = `
<div class="p-4 bg-gray-50 rounded-lg"><p class="text-sm text-gray-500">Total Profit</p><p class="text-2xl font-bold text-green-600">${data.total_profit_pips.toFixed(2)} pips</p></div>
<div class="p-4 bg-gray-50 rounded-lg"><p class="text-sm text-gray-500">Total Profit</p><p class="text-2xl font-bold text-green-600">${data.total_profit_usd.toFixed(2)} $</p></div>
<div class="p-4 bg-gray-50 rounded-lg"><p class="text-sm text-gray-500">Max Drawdown</p><p class="text-2xl font-bold text-red-600">${data.max_drawdown_percent.toFixed(2)}%</p></div>
<div class="p-4 bg-gray-50 rounded-lg"><p class="text-sm text-gray-500">Win Rate</p><p class="text-2xl font-bold text-blue-600">${data.win_rate_percent.toFixed(2)}%</p></div>
<div class="p-4 bg-gray-50 rounded-lg"><p class="text-sm text-gray-500">Total Trades</p><p class="text-2xl font-bold">${data.total_trades}</p></div>
@@ -126,8 +126,8 @@ document.addEventListener('DOMContentLoaded', () => {
if (data.trades && data.trades.length > 0) {
let logHtml = '<h4 class="text-lg font-semibold mt-6 mb-2">20 Trade Terakhir</h4><div class="text-xs font-mono border rounded p-2 bg-gray-50 max-h-64 overflow-y-auto">';
data.trades.forEach(trade => {
const profitClass = trade.profit_pips > 0 ? 'text-green-600' : 'text-red-600';
logHtml += `<p>Entry: ${trade.entry.toFixed(4)} | Exit: ${trade.exit.toFixed(4)} | Profit: <span class="${profitClass}">${trade.profit_pips.toFixed(2)} pips</span> | Reason: ${trade.reason}</p>`;
const profitClass = trade.profit > 0 ? 'text-green-600' : 'text-red-600';
logHtml += `<p>Entry: ${trade.entry.toFixed(4)} | Exit: ${trade.exit.toFixed(4)} | Profit: <span class="${profitClass}">${trade.profit.toFixed(2)}</span> | Reason: ${trade.reason}</p>`;
});
logHtml += '</div>';
resultsLog.innerHTML = logHtml;
+3 -3
View File
@@ -38,9 +38,9 @@ document.addEventListener('DOMContentLoaded', function() {
// Render Parameter Standar
let paramsHTML = `
<div class="grid grid-cols-2 gap-4">
<div><p class="text-gray-500">Lot Size</p><p class="font-semibold text-gray-800">${botData.lot_size}</p></div>
<div><p class="text-gray-500">Stop Loss</p><p class="font-semibold text-gray-800">${botData.sl_pips} pips</p></div>
<div><p class="text-gray-500">Take Profit</p><p class="font-semibold text-gray-800">${botData.tp_pips} pips</p></div>
<div><p class="text-gray-500">Risk per Trade</p><p class="font-semibold text-gray-800">${botData.lot_size}%</p></div>
<div><p class="text-gray-500">SL (ATR Multiplier)</p><p class="font-semibold text-gray-800">${botData.sl_pips}x ATR</p></div>
<div><p class="text-gray-500">TP (ATR Multiplier)</p><p class="font-semibold text-gray-800">${botData.tp_pips}x ATR</p></div>
<div><p class="text-gray-500">Interval</p><p class="font-semibold text-gray-800">${botData.check_interval_seconds}s</p></div>
</div>
`;
+7 -6
View File
@@ -77,8 +77,8 @@ document.addEventListener('DOMContentLoaded', function() {
<div class="text-sm text-gray-500">${bot.market}</div>
</td>
<td class="px-4 py-4 text-sm text-gray-500">
<div>Lot: ${bot.lot_size}</div>
<div>SL: ${bot.sl_pips} pips | TP: ${bot.tp_pips} pips</div>
<div>Risk: ${bot.lot_size}%</div>
<div>SL: ${bot.sl_pips}x ATR | TP: ${bot.tp_pips}x ATR</div>
</td>
<td class="px-4 py-4 text-sm text-gray-500">
<div>Strategi: ${bot.strategy_name}</div>
@@ -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;
+2 -2
View File
@@ -72,8 +72,8 @@
<!-- Baris 2: Lot, SL, TP -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<label for="lot_size" class="block mb-2 text-sm font-medium text-gray-900">Ukuran Lot</label>
<input type="number" name="lot_size" id="lot_size" value="0.01" step="0.01" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" required>
<label for="risk_percent" class="block mb-2 text-sm font-medium text-gray-900">Risk per Trade (%)</label>
<input type="number" name="risk_percent" id="risk_percent" value="1.0" step="0.1" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" required>
</div>
<div>
<label for="sl_atr_multiplier" class="block mb-2 text-sm font-medium text-gray-900">SL (ATR Multiplier)</label>