Files
GifariKemal 7af9183af3 feat: Smart AI Trading Bot for XAUUSD with ML and SMC
- XGBoost ML model with 37 features for market direction prediction
- Smart Money Concepts (SMC): Order Blocks, FVG, BOS, CHoCH
- HMM market regime detection (trending/ranging/volatile)
- ATR-based stop loss with 1.5 ATR minimum distance
- Broker-level SL protection with fallback
- Time-based exit (max 6 hours per trade)
- Session-aware trading optimized for London/NY overlap
- Auto-retraining based on market conditions
- Telegram notifications and web dashboard
- Backtest results: 63.9% win rate, 2.64 profit factor, 4.83 Sharpe

Backtest period: Jan 2025 - Feb 2026, 654 trades, $4,189 net P/L

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 09:01:35 +07:00

456 lines
16 KiB
Python

"""
Backtest Simulation - 1 Month Historical Data
=============================================
Simulasi sistem trading dengan data market real 1 bulan kebelakang.
"""
import os
import sys
from datetime import datetime, timedelta
from dataclasses import dataclass
from typing import List, Optional, Tuple
import polars as pl
from dotenv import load_dotenv
from loguru import logger
# Configure logging
logger.remove()
logger.add(sys.stdout, format="<green>{time:HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{message}</cyan>", level="INFO")
load_dotenv()
@dataclass
class SimulatedTrade:
"""Simulated trade result."""
entry_time: datetime
exit_time: datetime
direction: str
entry_price: float
exit_price: float
lot_size: float
profit: float
reason: str
ml_confidence: float
smc_signal: bool
market_quality: str
def run_backtest_1month():
"""Run 1 month backtest simulation."""
print("=" * 70)
print("BACKTEST SIMULATION - 1 MONTH HISTORICAL DATA")
print("=" * 70)
print()
# Import components
from src.mt5_connector import MT5Connector
from src.feature_eng import FeatureEngineer
from src.ml_model import TradingModel
from src.smc_polars import SMCAnalyzer
from src.regime_detector import MarketRegimeDetector
from src.dynamic_confidence import create_dynamic_confidence
from src.smart_risk_manager import create_smart_risk_manager
from src.session_filter import SessionFilter
# Connect to MT5
mt5 = MT5Connector(
login=int(os.getenv('MT5_LOGIN')),
password=os.getenv('MT5_PASSWORD'),
server=os.getenv('MT5_SERVER'),
)
if not mt5.connect():
print("Failed to connect to MT5")
return
print(f"Connected to MT5")
print(f"Balance: ${mt5.account_balance:,.2f}")
print()
# Initialize components
feature_eng = FeatureEngineer()
ml_model = TradingModel()
ml_model.load("models/xgboost_model.pkl")
smc = SMCAnalyzer()
regime = MarketRegimeDetector()
regime.load()
dynamic_conf = create_dynamic_confidence()
risk_manager = create_smart_risk_manager(mt5.account_balance)
session_filter = SessionFilter()
# Fetch 1 month of M5 data (~8640 bars)
# M5 = 5 minutes, 1 month = 30 days * 24 hours * 12 bars/hour = 8640
symbol = "XAUUSD"
print("Fetching 1 month of historical data...")
df = mt5.get_market_data(symbol, "M5", count=9000) # ~1 month of M5 data
if df is None or len(df) == 0:
print("Failed to fetch historical data")
mt5.disconnect()
return
print(f"Fetched {len(df)} bars of historical data")
print(f"Date range: {df['time'][0]} to {df['time'][-1]}")
# Calculate date range
start_date = df['time'][0]
end_date = df['time'][-1]
days_covered = (end_date - start_date).days
print(f"Period covered: {days_covered} days")
print()
# Add all features
print("Calculating features...")
df = feature_eng.calculate_all(df)
df = smc.calculate_all(df)
df = regime.predict(df)
# Get feature columns for ML
feature_cols = [c for c in df.columns if c in ml_model.feature_names]
print(f"Using {len(feature_cols)} features for ML prediction")
print()
print("=" * 70)
print("IMPROVED SYSTEM SETTINGS:")
print("=" * 70)
print(f" Min ML confidence : 65%")
print(f" ML-only threshold : 75%+")
print(f" SMC+ML requirement : Both MUST agree (65%+)")
print(f" Session filter : Only London, NY, Overlap")
print(f" Trade cooldown : 60 bars (5 hours)")
print(f" Max lot size : {risk_manager.max_lot_size}")
print(f" Max loss/trade : ${risk_manager.max_loss_per_trade}")
print("=" * 70)
print()
# Simulation parameters
simulated_trades: List[SimulatedTrade] = []
initial_balance = mt5.account_balance
current_balance = initial_balance
last_trade_idx = -100 # Start with no cooldown
cooldown_bars = 60 # 5 hours cooldown (60 * 5min = 300min = 5h)
# Stats
total_signals = 0
skipped_low_confidence = 0
skipped_no_agreement = 0
skipped_poor_quality = 0
skipped_cooldown = 0
skipped_session = 0
skipped_wrong_direction = 0
# Daily tracking
daily_pnl = {}
print("Running simulation...")
print("-" * 70)
# Simulate through historical data (skip first 300 bars for indicator warmup)
for i in range(300, len(df) - 60):
# Get data up to this point
current_df = df.head(i + 1)
current_price = current_df['close'][-1]
current_time = current_df['time'][-1]
current_date = current_time.date()
# Initialize daily PnL tracking
if current_date not in daily_pnl:
daily_pnl[current_date] = 0
# Check session (simplified - check hour)
hour = current_time.hour
# London: 14:00-22:00 WIB, NY: 19:00-04:00 WIB, Overlap: 19:00-22:00 WIB
# In UTC: London 07:00-15:00, NY 12:00-21:00, Overlap 12:00-15:00
is_good_session = (7 <= hour <= 21) # Simplified: 07:00-21:00 UTC
if not is_good_session:
continue
# ML Prediction
ml_pred = ml_model.predict(current_df, feature_cols)
# Skip if ML confidence too low (min 65%)
if ml_pred.confidence < 0.65:
skipped_low_confidence += 1
continue
total_signals += 1
# Check cooldown
if i - last_trade_idx < cooldown_bars:
skipped_cooldown += 1
continue
# SMC Signal
smc_signal = smc.generate_signal(current_df)
has_smc = smc_signal is not None
# Get market quality (simplified)
market_quality = "good"
# Entry decision
should_trade = False
trade_direction = None
trade_reason = ""
# Rule 1: ML-only needs 75%+
if not has_smc:
if ml_pred.confidence >= 0.75:
should_trade = True
trade_direction = ml_pred.signal
trade_reason = f"ML-ONLY ({ml_pred.confidence:.0%})"
else:
skipped_low_confidence += 1
continue
else:
# Rule 2: SMC + ML must agree
ml_agrees = (
(smc_signal.signal_type == "BUY" and ml_pred.signal == "BUY") or
(smc_signal.signal_type == "SELL" and ml_pred.signal == "SELL")
)
if ml_agrees and ml_pred.confidence >= 0.65:
should_trade = True
trade_direction = ml_pred.signal
trade_reason = f"SMC+ML ({ml_pred.confidence:.0%})"
else:
skipped_no_agreement += 1
continue
if not should_trade or trade_direction not in ["BUY", "SELL"]:
continue
# Simulate trade execution
entry_price = current_price
lot_size = risk_manager.base_lot_size # 0.01
# Look ahead to find exit (simplified: 12-60 bars, ~1-5 hours)
# Use ATR-based TP/SL
atr = current_df['atr'][-1] if 'atr' in current_df.columns else current_price * 0.003
tp_distance = atr * 2.0 # 2 ATR for TP
sl_distance = atr * 1.5 # 1.5 ATR for SL
if trade_direction == "BUY":
tp_price = entry_price + tp_distance
sl_price = entry_price - sl_distance
else:
tp_price = entry_price - tp_distance
sl_price = entry_price + sl_distance
# Simulate price movement over next 60 bars
exit_price = entry_price
exit_time = current_time
exit_reason = "TIMEOUT"
for j in range(1, min(61, len(df) - i)):
future_high = df['high'][i + j]
future_low = df['low'][i + j]
future_time = df['time'][i + j]
if trade_direction == "BUY":
# Check SL first
if future_low <= sl_price:
exit_price = sl_price
exit_time = future_time
exit_reason = "SL"
break
# Check TP
if future_high >= tp_price:
exit_price = tp_price
exit_time = future_time
exit_reason = "TP"
break
else: # SELL
# Check SL first
if future_high >= sl_price:
exit_price = sl_price
exit_time = future_time
exit_reason = "SL"
break
# Check TP
if future_low <= tp_price:
exit_price = tp_price
exit_time = future_time
exit_reason = "TP"
break
exit_price = df['close'][i + j]
exit_time = future_time
# Calculate profit
if trade_direction == "BUY":
price_diff = exit_price - entry_price
else:
price_diff = entry_price - exit_price
# Gold: 1 lot = $100 per point, 0.01 lot = $1 per point
profit = price_diff * lot_size * 100
# Apply max loss limit
if profit < -risk_manager.max_loss_per_trade:
profit = -risk_manager.max_loss_per_trade
# Record trade
trade = SimulatedTrade(
entry_time=current_time,
exit_time=exit_time,
direction=trade_direction,
entry_price=entry_price,
exit_price=exit_price,
lot_size=lot_size,
profit=profit,
reason=trade_reason,
ml_confidence=ml_pred.confidence,
smc_signal=has_smc,
market_quality=market_quality,
)
simulated_trades.append(trade)
current_balance += profit
last_trade_idx = i
# Track daily PnL
daily_pnl[current_date] = daily_pnl.get(current_date, 0) + profit
# Print trade (limit output)
if len(simulated_trades) <= 30 or len(simulated_trades) % 10 == 0:
result = "WIN" if profit > 0 else "LOSS"
print(f" {current_time.strftime('%Y-%m-%d %H:%M')} | {trade_direction} | {trade_reason} | ${profit:+.2f} [{result}] ({exit_reason})")
print("-" * 70)
print()
# Calculate statistics
total_trades = len(simulated_trades)
if total_trades > 0:
winning_trades = [t for t in simulated_trades if t.profit > 0]
losing_trades = [t for t in simulated_trades if t.profit <= 0]
win_count = len(winning_trades)
loss_count = len(losing_trades)
win_rate = (win_count / total_trades) * 100
total_profit = sum(t.profit for t in simulated_trades)
avg_win = sum(t.profit for t in winning_trades) / win_count if win_count > 0 else 0
avg_loss = sum(t.profit for t in losing_trades) / loss_count if loss_count > 0 else 0
# Profit factor
gross_profit = sum(t.profit for t in winning_trades)
gross_loss = abs(sum(t.profit for t in losing_trades))
profit_factor = gross_profit / gross_loss if gross_loss > 0 else float('inf')
# Max drawdown
running_balance = initial_balance
peak_balance = initial_balance
max_drawdown = 0
max_drawdown_pct = 0
for trade in simulated_trades:
running_balance += trade.profit
if running_balance > peak_balance:
peak_balance = running_balance
drawdown = peak_balance - running_balance
drawdown_pct = (drawdown / peak_balance) * 100
if drawdown > max_drawdown:
max_drawdown = drawdown
max_drawdown_pct = drawdown_pct
# Consecutive wins/losses
max_consecutive_wins = 0
max_consecutive_losses = 0
current_wins = 0
current_losses = 0
for trade in simulated_trades:
if trade.profit > 0:
current_wins += 1
current_losses = 0
max_consecutive_wins = max(max_consecutive_wins, current_wins)
else:
current_losses += 1
current_wins = 0
max_consecutive_losses = max(max_consecutive_losses, current_losses)
print("=" * 70)
print("BACKTEST RESULTS - 1 MONTH")
print("=" * 70)
print()
print(f" Period : {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')} ({days_covered} days)")
print()
print(f" Initial Balance : ${initial_balance:,.2f}")
print(f" Final Balance : ${current_balance:,.2f}")
print(f" Total P/L : ${total_profit:+,.2f} ({(total_profit/initial_balance)*100:+.2f}%)")
print()
print(f" Total Trades : {total_trades}")
print(f" Winning Trades : {win_count}")
print(f" Losing Trades : {loss_count}")
print(f" Win Rate : {win_rate:.1f}%")
print()
print(f" Average Win : ${avg_win:+.2f}")
print(f" Average Loss : ${avg_loss:.2f}")
print(f" Profit Factor : {profit_factor:.2f}")
print()
print(f" Max Drawdown : ${max_drawdown:,.2f} ({max_drawdown_pct:.1f}%)")
print(f" Max Consec. Wins : {max_consecutive_wins}")
print(f" Max Consec. Loss : {max_consecutive_losses}")
print()
# Signals Analysis
print(" Signals Analysis:")
print(f" Total ML signals (65%+) : {total_signals}")
print(f" Skipped (low conf) : {skipped_low_confidence}")
print(f" Skipped (no agreement) : {skipped_no_agreement}")
print(f" Skipped (cooldown) : {skipped_cooldown}")
print(f" Executed trades : {total_trades}")
print()
# Trade breakdown
ml_only_trades = [t for t in simulated_trades if "ML-ONLY" in t.reason]
smc_ml_trades = [t for t in simulated_trades if "SMC+ML" in t.reason]
print(" Trade Type Breakdown:")
if ml_only_trades:
ml_wins = len([t for t in ml_only_trades if t.profit > 0])
ml_profit = sum(t.profit for t in ml_only_trades)
print(f" ML-ONLY trades : {len(ml_only_trades)} (Win: {ml_wins}, WR: {ml_wins/len(ml_only_trades)*100:.0f}%, P/L: ${ml_profit:+.2f})")
if smc_ml_trades:
smc_wins = len([t for t in smc_ml_trades if t.profit > 0])
smc_profit = sum(t.profit for t in smc_ml_trades)
print(f" SMC+ML trades : {len(smc_ml_trades)} (Win: {smc_wins}, WR: {smc_wins/len(smc_ml_trades)*100:.0f}%, P/L: ${smc_profit:+.2f})")
print()
# Daily breakdown
print(" Daily Performance (last 10 days with trades):")
sorted_days = sorted(daily_pnl.items(), key=lambda x: x[0], reverse=True)
days_with_trades = [(d, p) for d, p in sorted_days if p != 0][:10]
for date, pnl in days_with_trades:
result = "[+]" if pnl > 0 else "[-]"
print(f" {date} : ${pnl:+.2f} {result}")
print()
# Monthly projection
trades_per_day = total_trades / days_covered if days_covered > 0 else 0
profit_per_day = total_profit / days_covered if days_covered > 0 else 0
monthly_projection = profit_per_day * 30
print(" Projections:")
print(f" Avg trades/day : {trades_per_day:.1f}")
print(f" Avg profit/day : ${profit_per_day:+.2f}")
print(f" Monthly projection: ${monthly_projection:+.2f}")
else:
print("No trades executed in simulation period.")
print(f" Total signals checked: {total_signals}")
print(f" Skipped (low confidence): {skipped_low_confidence}")
print(f" Skipped (no agreement): {skipped_no_agreement}")
print(f" Skipped (cooldown): {skipped_cooldown}")
print()
print("=" * 70)
print("SIMULATION COMPLETE")
print("=" * 70)
mt5.disconnect()
if __name__ == "__main__":
run_backtest_1month()