2025-08-02 21:26:22 +08:00
|
|
|
# core/backtesting/engine.py
|
|
|
|
|
|
|
|
|
|
from core.strategies.strategy_map import STRATEGY_MAP
|
|
|
|
|
|
|
|
|
|
def run_backtest(strategy_id, params, historical_data_df):
|
|
|
|
|
"""
|
|
|
|
|
Menjalankan simulasi backtesting untuk strategi tertentu pada data historis.
|
2025-08-08 10:39:03 +08:00
|
|
|
VERSI OPTIMIZED: Indikator dihitung sekali di awal.
|
2025-08-02 21:26:22 +08:00
|
|
|
"""
|
|
|
|
|
strategy_class = STRATEGY_MAP.get(strategy_id)
|
|
|
|
|
if not strategy_class:
|
|
|
|
|
return {"error": "Strategi tidak ditemukan"}
|
|
|
|
|
|
2025-08-08 10:39:03 +08:00
|
|
|
# --- LANGKAH 1: Hitung semua indikator SEKALI di awal ---
|
|
|
|
|
class MockBot:
|
|
|
|
|
def __init__(self):
|
|
|
|
|
self.market_for_mt5 = "BACKTEST"
|
|
|
|
|
self.timeframe = "H1"
|
|
|
|
|
self.tf_map = {}
|
|
|
|
|
|
|
|
|
|
strategy_instance = strategy_class(bot_instance=MockBot(), params=params)
|
|
|
|
|
|
|
|
|
|
# Panggil metode baru 'analyze_df' untuk pra-perhitungan indikator
|
|
|
|
|
df_with_indicators = strategy_instance.analyze_df(historical_data_df.copy())
|
|
|
|
|
|
|
|
|
|
if df_with_indicators.empty:
|
|
|
|
|
return {"error": "Gagal menghasilkan data indikator. Periksa panjang data input."}
|
|
|
|
|
|
|
|
|
|
strategy_name = strategy_instance.name
|
|
|
|
|
|
|
|
|
|
# --- LANGKAH 2: Inisialisasi state backtesting ---
|
2025-08-02 21:26:22 +08:00
|
|
|
trades = []
|
|
|
|
|
in_position = False
|
2025-08-08 10:39:03 +08:00
|
|
|
initial_capital = 10000
|
2025-08-02 21:26:22 +08:00
|
|
|
capital = initial_capital
|
|
|
|
|
equity_curve = [initial_capital]
|
|
|
|
|
peak_equity = initial_capital
|
|
|
|
|
max_drawdown = 0.0
|
|
|
|
|
position_type = None
|
|
|
|
|
entry_price = 0.0
|
|
|
|
|
sl_pips = params.get('sl_pips', 100)
|
|
|
|
|
tp_pips = params.get('tp_pips', 200)
|
|
|
|
|
|
2025-08-08 10:39:03 +08:00
|
|
|
symbol_name = historical_data_df.columns[0].upper()
|
|
|
|
|
pip_size = 0.0001 # Default untuk Forex standar
|
|
|
|
|
if 'JPY' in symbol_name:
|
|
|
|
|
pip_size = 0.01
|
|
|
|
|
elif 'XAU' in symbol_name or 'XAG' in symbol_name: # Emas atau Perak
|
|
|
|
|
pip_size = 0.01
|
2025-08-02 21:26:22 +08:00
|
|
|
|
2025-08-08 10:39:03 +08:00
|
|
|
# Tentukan nilai per pip berdasarkan simbol (untuk lot 0.01)
|
|
|
|
|
if 'XAU' in symbol_name or 'XAG' in symbol_name: # Emas atau Perak
|
|
|
|
|
# Untuk 0.01 lot (1 oz), pergerakan harga $0.01 = profit/loss $0.01
|
|
|
|
|
value_per_pip = 0.01
|
|
|
|
|
else:
|
|
|
|
|
# Untuk Forex (misal EURUSD), 0.01 lot, pergerakan 1 pip = profit/loss $0.1
|
|
|
|
|
# Ini adalah asumsi umum, untuk JPY pairs nilainya bisa sedikit berbeda
|
|
|
|
|
value_per_pip = 0.1
|
2025-08-02 21:26:22 +08:00
|
|
|
|
2025-08-08 10:39:03 +08:00
|
|
|
# --- LANGKAH 3: Loop melalui data yang sudah ada indikatornya ---
|
|
|
|
|
for i in range(1, len(df_with_indicators)):
|
|
|
|
|
current_bar = df_with_indicators.iloc[i]
|
|
|
|
|
signal = current_bar.get("signal", "HOLD")
|
|
|
|
|
current_price = current_bar['close']
|
2025-08-02 21:26:22 +08:00
|
|
|
|
|
|
|
|
# Cek SL/TP jika sedang dalam posisi
|
|
|
|
|
if in_position:
|
|
|
|
|
profit = 0
|
|
|
|
|
if position_type == 'BUY':
|
2025-08-08 10:39:03 +08:00
|
|
|
profit_pips = (current_price - entry_price) / pip_size
|
|
|
|
|
if current_price <= entry_price - (sl_pips * pip_size):
|
|
|
|
|
trades.append({'entry': entry_price, 'exit': current_price, 'profit_pips': -sl_pips, 'reason': 'SL'})
|
|
|
|
|
capital -= sl_pips * value_per_pip
|
2025-08-02 21:26:22 +08:00
|
|
|
in_position = False
|
2025-08-08 10:39:03 +08:00
|
|
|
elif current_price >= entry_price + (tp_pips * pip_size):
|
|
|
|
|
trades.append({'entry': entry_price, 'exit': current_price, 'profit_pips': tp_pips, 'reason': 'TP'})
|
|
|
|
|
capital += tp_pips * value_per_pip
|
2025-08-02 21:26:22 +08:00
|
|
|
in_position = False
|
|
|
|
|
elif position_type == 'SELL':
|
2025-08-08 10:39:03 +08:00
|
|
|
profit_pips = (entry_price - current_price) / pip_size
|
|
|
|
|
if current_price >= entry_price + (sl_pips * pip_size):
|
|
|
|
|
trades.append({'entry': entry_price, 'exit': current_price, 'profit_pips': -sl_pips, 'reason': 'SL'})
|
|
|
|
|
capital -= sl_pips * value_per_pip
|
2025-08-02 21:26:22 +08:00
|
|
|
in_position = False
|
2025-08-08 10:39:03 +08:00
|
|
|
elif current_price <= entry_price - (tp_pips * pip_size):
|
|
|
|
|
trades.append({'entry': entry_price, 'exit': current_price, 'profit_pips': tp_pips, 'reason': 'TP'})
|
|
|
|
|
capital += tp_pips * value_per_pip
|
2025-08-02 21:26:22 +08:00
|
|
|
in_position = False
|
2025-08-08 10:39:03 +08:00
|
|
|
|
2025-08-02 21:26:22 +08:00
|
|
|
if not in_position: # Jika posisi baru saja ditutup
|
|
|
|
|
equity_curve.append(capital)
|
|
|
|
|
peak_equity = max(peak_equity, capital)
|
2025-08-08 10:39:03 +08:00
|
|
|
drawdown = (peak_equity - capital) / peak_equity if peak_equity > 0 else 0
|
2025-08-02 21:26:22 +08:00
|
|
|
max_drawdown = max(max_drawdown, drawdown)
|
|
|
|
|
|
|
|
|
|
# Cek sinyal baru
|
|
|
|
|
if signal == 'BUY' and not in_position:
|
|
|
|
|
in_position = True
|
|
|
|
|
position_type = 'BUY'
|
|
|
|
|
entry_price = current_price
|
|
|
|
|
elif signal == 'SELL' and not in_position:
|
|
|
|
|
in_position = True
|
|
|
|
|
position_type = 'SELL'
|
|
|
|
|
entry_price = current_price
|
|
|
|
|
elif (signal == 'SELL' and in_position and position_type == 'BUY') or \
|
|
|
|
|
(signal == 'BUY' and in_position and position_type == 'SELL'):
|
|
|
|
|
# Sinyal berlawanan, tutup posisi lama
|
2025-08-08 10:39:03 +08:00
|
|
|
profit_pips = ((current_price - entry_price) if position_type == 'BUY' else (entry_price - current_price)) / pip_size # Calculate pips
|
|
|
|
|
trades.append({'entry': entry_price, 'exit': current_price, 'profit_pips': profit_pips, 'reason': 'Signal Flip'}) # Log trade
|
2025-08-02 21:26:22 +08:00
|
|
|
capital += profit_pips * value_per_pip
|
|
|
|
|
equity_curve.append(capital)
|
|
|
|
|
peak_equity = max(peak_equity, capital)
|
2025-08-08 10:39:03 +08:00
|
|
|
drawdown = (peak_equity - capital) / peak_equity if peak_equity > 0 else 0
|
2025-08-02 21:26:22 +08:00
|
|
|
max_drawdown = max(max_drawdown, drawdown)
|
|
|
|
|
in_position = False
|
|
|
|
|
|
|
|
|
|
# Hitung hasil akhir
|
|
|
|
|
total_profit_pips = sum(trade['profit_pips'] for trade in trades)
|
|
|
|
|
wins = len([trade for trade in trades if trade['profit_pips'] > 0])
|
|
|
|
|
losses = len(trades) - wins
|
|
|
|
|
win_rate = (wins / len(trades) * 100) if trades else 0
|
|
|
|
|
|
|
|
|
|
return {
|
2025-08-08 10:39:03 +08:00
|
|
|
"strategy_name": strategy_name,
|
2025-08-02 21:26:22 +08:00
|
|
|
"total_trades": len(trades),
|
|
|
|
|
"total_profit_pips": total_profit_pips,
|
|
|
|
|
"win_rate_percent": win_rate,
|
|
|
|
|
"wins": wins,
|
|
|
|
|
"losses": losses,
|
|
|
|
|
"max_drawdown_percent": max_drawdown * 100,
|
|
|
|
|
"equity_curve": equity_curve,
|
2025-08-08 10:39:03 +08:00
|
|
|
"trades": trades[-20:]
|
2025-08-02 21:26:22 +08:00
|
|
|
}
|