Files
XauBot/backtests/archive/backtest_comparison_v2.py
T
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

513 lines
18 KiB
Python

"""
BACKTEST COMPARISON v2 - SMC-only vs ML+SMC
===========================================
Compare different signal strategies:
- System A: SMC-only (original profitable backtest)
- System B: ML+SMC during Golden Time (new conservative)
- System C: Tighter Smart Hold (50% cut vs 80% cut)
"""
import polars as pl
import numpy as np
import pickle
from datetime import datetime, timedelta, date
from dataclasses import dataclass
from typing import List, Optional, Tuple, Dict
from loguru import logger
import sys
logger.remove()
logger.add(sys.stdout, format="<green>{time:HH:mm:ss}</green> | <level>{level:<8}</level> | <cyan>{message}</cyan>", level="INFO")
def get_session(dt: datetime) -> Tuple[str, bool]:
"""Get trading session and if it's golden time."""
hour = dt.hour
if 19 <= hour <= 23:
return "London-NY Overlap", True # GOLDEN TIME
elif 14 <= hour < 19:
return "London", False
elif 5 <= hour < 14:
return "Sydney/Tokyo", False
else:
return "Off-hours", False
def hours_to_golden(dt: datetime) -> float:
"""Calculate hours until golden time (19:00 WIB)."""
current_hour = dt.hour + dt.minute / 60
golden_start = 19.0
if 19 <= current_hour <= 23:
return 0 # Already in golden time
elif current_hour < 19:
return golden_start - current_hour
else: # After 23:00
return (24 - current_hour) + golden_start
@dataclass
class Trade:
entry_time: datetime
exit_time: datetime
direction: str
entry_price: float
exit_price: float
pnl: float
exit_reason: str
hold_time_hours: float
session: str
is_golden: bool
class MLSimulator:
"""Simulate ML predictions based on loaded model."""
def __init__(self, model_path: str = "models/xgboost_model.pkl"):
self.model = None
self.features = None
try:
with open(model_path, "rb") as f:
data = pickle.load(f)
if isinstance(data, dict):
self.model = data.get("model")
self.features = data.get("features", [])
else:
self.model = data
logger.info(f"ML model loaded for backtest")
except Exception as e:
logger.warning(f"Could not load ML model: {e}")
def predict(self, df: pl.DataFrame, idx: int) -> Tuple[str, float]:
"""Predict signal and confidence at given index."""
if self.model is None:
return "HOLD", 0.50
try:
# Get features for this row
row = df.row(idx, named=True)
# Simple momentum-based prediction for simulation
# (Real model would use actual features)
close = row.get("close", 0)
sma_20 = row.get("sma_20", close)
rsi = row.get("rsi", 50)
# Simulate prediction
if close > sma_20 and rsi < 70:
return "BUY", 0.55 + (70 - rsi) / 200
elif close < sma_20 and rsi > 30:
return "SELL", 0.55 + (rsi - 30) / 200
else:
return "HOLD", 0.50
except Exception:
return "HOLD", 0.50
def run_comparison():
"""Run comprehensive comparison backtest."""
print("=" * 80)
print("BACKTEST COMPARISON v2: SMC-only vs ML+SMC")
print("=" * 80)
# Load data
print("\n[1] Loading data...")
import MetaTrader5 as mt5
from src.feature_eng import FeatureEngineer
from src.smc_polars import SMCAnalyzer
if not mt5.initialize():
print("MT5 init failed")
return
rates = mt5.copy_rates_from_pos("XAUUSD", mt5.TIMEFRAME_M15, 0, 40000)
mt5.shutdown()
if rates is None:
print("Failed to get data")
return
df = pl.DataFrame({
"time": [datetime.fromtimestamp(r[0]) for r in rates],
"open": [r[1] for r in rates],
"high": [r[2] for r in rates],
"low": [r[3] for r in rates],
"close": [r[4] for r in rates],
"volume": [r[5] for r in rates],
})
print(f" Loaded {len(df)} bars")
print(f" Range: {df['time'][0]} to {df['time'][-1]}")
# Calculate features
print("\n[2] Calculating features...")
fe = FeatureEngineer()
df = fe.calculate_all(df)
# Parameters
lot_size = 0.02
initial_capital = 5000.0
max_loss_per_trade = 50.0
confidence_threshold = 0.70
min_bars_between_trades = 4
# Initialize ML simulator
ml_sim = MLSimulator()
print("\n[3] Running backtests...")
print(f" Lot size: {lot_size}")
print(f" Initial capital: ${initial_capital}")
print(f" Max loss per trade: ${max_loss_per_trade}")
# ========================================
# SYSTEM A: SMC-ONLY (Original Backtest)
# ========================================
print("\n" + "=" * 80)
print("SYSTEM A: SMC-ONLY (No ML requirement)")
print(" - Trade on SMC signal only")
print(" - Cut loss at 80% of max")
print("=" * 80)
trades_a = run_system(
df, lot_size, initial_capital, max_loss_per_trade,
confidence_threshold, min_bars_between_trades,
ml_sim, system_type="SMC_ONLY", cut_loss_pct=0.80
)
# ========================================
# SYSTEM B: ML+SMC during Golden Time
# ========================================
print("\n" + "=" * 80)
print("SYSTEM B: ML+SMC during Golden Time")
print(" - Golden Time (19:00-23:00): Require ML+SMC alignment")
print(" - Other times: SMC-only")
print(" - Cut loss at 80% of max")
print("=" * 80)
trades_b = run_system(
df, lot_size, initial_capital, max_loss_per_trade,
confidence_threshold, min_bars_between_trades,
ml_sim, system_type="ML_SMC_GOLDEN", cut_loss_pct=0.80
)
# ========================================
# SYSTEM C: Tighter Smart Hold
# ========================================
print("\n" + "=" * 80)
print("SYSTEM C: Tighter Smart Hold")
print(" - SMC-only mode")
print(" - Cut loss at 50% of max (tighter)")
print("=" * 80)
trades_c = run_system(
df, lot_size, initial_capital, max_loss_per_trade,
confidence_threshold, min_bars_between_trades,
ml_sim, system_type="SMC_ONLY", cut_loss_pct=0.50
)
# ========================================
# SYSTEM D: ML+SMC + Tighter Hold
# ========================================
print("\n" + "=" * 80)
print("SYSTEM D: ML+SMC + Tighter Hold (NEW LIVE SYSTEM)")
print(" - Golden Time: Require ML+SMC alignment")
print(" - Cut loss at 50% of max")
print("=" * 80)
trades_d = run_system(
df, lot_size, initial_capital, max_loss_per_trade,
confidence_threshold, min_bars_between_trades,
ml_sim, system_type="ML_SMC_GOLDEN", cut_loss_pct=0.50
)
# ========================================
# COMPARISON RESULTS
# ========================================
print("\n" + "=" * 80)
print("COMPARISON RESULTS")
print("=" * 80)
results = []
for name, trades in [
("A: SMC-only (80% cut)", trades_a),
("B: ML+SMC Golden (80% cut)", trades_b),
("C: SMC-only (50% cut)", trades_c),
("D: ML+SMC + 50% cut (NEW)", trades_d),
]:
stats = calc_stats(trades, name, initial_capital)
results.append(stats)
print_stats(stats)
# Summary table
print("\n" + "=" * 80)
print("SUMMARY TABLE")
print("=" * 80)
print(f"{'System':<30} {'Trades':>8} {'Win%':>8} {'P/L':>12} {'PF':>8} {'MaxDD':>10}")
print("-" * 80)
for r in results:
print(f"{r['name']:<30} {r['trades']:>8} {r['win_rate']:>7.1f}% ${r['total_pnl']:>10.2f} {r['profit_factor']:>7.2f} {r['max_drawdown']:>9.2f}%")
# Golden Time breakdown
print("\n" + "=" * 80)
print("GOLDEN TIME BREAKDOWN")
print("=" * 80)
for name, trades in [
("A: SMC-only (80%)", trades_a),
("D: ML+SMC + 50% (NEW)", trades_d),
]:
golden_trades = [t for t in trades if t.is_golden]
non_golden_trades = [t for t in trades if not t.is_golden]
print(f"\n{name}:")
if golden_trades:
golden_pnl = sum(t.pnl for t in golden_trades)
golden_wins = len([t for t in golden_trades if t.pnl > 0])
print(f" Golden Time: {len(golden_trades)} trades, {golden_wins}/{len(golden_trades)} wins ({100*golden_wins/len(golden_trades):.1f}%), P/L: ${golden_pnl:.2f}")
if non_golden_trades:
ng_pnl = sum(t.pnl for t in non_golden_trades)
ng_wins = len([t for t in non_golden_trades if t.pnl > 0])
print(f" Non-Golden: {len(non_golden_trades)} trades, {ng_wins}/{len(non_golden_trades)} wins ({100*ng_wins/len(non_golden_trades):.1f}%), P/L: ${ng_pnl:.2f}")
print("\n" + "=" * 80)
print("RECOMMENDATION")
print("=" * 80)
best = max(results, key=lambda x: x['total_pnl'])
safest = min(results, key=lambda x: x['max_drawdown'])
print(f" Most Profitable: {best['name']} (${best['total_pnl']:.2f})")
print(f" Lowest Drawdown: {safest['name']} ({safest['max_drawdown']:.2f}%)")
if best['name'] == safest['name']:
print(f"\n ✓ RECOMMENDED: {best['name']}")
else:
print(f"\n Trade-off detected:")
print(f" - For max profit: {best['name']}")
print(f" - For safety: {safest['name']}")
def run_system(
df: pl.DataFrame,
lot_size: float,
initial_capital: float,
max_loss_per_trade: float,
confidence_threshold: float,
min_bars_between_trades: int,
ml_sim: MLSimulator,
system_type: str, # "SMC_ONLY" or "ML_SMC_GOLDEN"
cut_loss_pct: float, # 0.80 or 0.50
) -> List[Trade]:
"""Run backtest for a specific system configuration."""
from src.smc_polars import SMCAnalyzer
trades: List[Trade] = []
position = None
capital = initial_capital
last_trade_idx = -min_bars_between_trades
for idx in range(200, len(df) - 1):
row = df.row(idx, named=True)
current_time = row["time"]
if current_time.date() < date(2025, 6, 1):
continue
if current_time.date() > date(2026, 2, 5):
break
close = row["close"]
high = row["high"]
low = row["low"]
session, is_golden = get_session(current_time)
hrs_to_golden = hours_to_golden(current_time)
# Manage position
if position is not None:
exit_reason = None
exit_price = None
# Calculate current P/L
if position["direction"] == "BUY":
current_pnl = (close - position["entry"]) * lot_size * 100
if high >= position["tp"]:
exit_price = position["tp"]
exit_reason = "TP_HIT"
else:
current_pnl = (position["entry"] - close) * lot_size * 100
if low <= position["tp"]:
exit_price = position["tp"]
exit_reason = "TP_HIT"
# Smart Hold Logic
if exit_reason is None:
loss_percent = abs(current_pnl) / max_loss_per_trade if current_pnl < 0 else 0
if current_pnl < 0:
# Max loss - use cut_loss_pct parameter
if loss_percent >= cut_loss_pct:
exit_price = close
exit_reason = f"CUT_LOSS_{int(cut_loss_pct*100)}PCT"
# Smart Hold - only if loss < 30% and golden near
elif loss_percent < 0.30 and hrs_to_golden <= 3:
pass # HOLD
# Medium loss, not near golden - cut
elif loss_percent >= 0.30 and hrs_to_golden > 3:
exit_price = close
exit_reason = "CUT_LOSS_NO_GOLDEN"
# Check reversal
df_slice = df.slice(max(0, idx - 200), min(201, idx + 1))
smc_temp = SMCAnalyzer()
df_slice = smc_temp.calculate_all(df_slice)
signal = smc_temp.generate_signal(df_slice)
if signal and signal.confidence >= 0.75:
if position["direction"] == "BUY" and signal.signal_type == "SELL":
exit_price = close
exit_reason = "REVERSAL"
elif position["direction"] == "SELL" and signal.signal_type == "BUY":
exit_price = close
exit_reason = "REVERSAL"
# Execute exit
if exit_reason:
if position["direction"] == "BUY":
pnl = (exit_price - position["entry"]) * lot_size * 100
else:
pnl = (position["entry"] - exit_price) * lot_size * 100
capital += pnl
hold_hours = (current_time - position["time"]).total_seconds() / 3600
trades.append(Trade(
entry_time=position["time"],
exit_time=current_time,
direction=position["direction"],
entry_price=position["entry"],
exit_price=exit_price,
pnl=pnl,
exit_reason=exit_reason,
hold_time_hours=hold_hours,
session=position["session"],
is_golden=position["is_golden"],
))
position = None
# Check for new signal
if position is None and (idx - last_trade_idx) >= min_bars_between_trades:
df_slice = df.slice(max(0, idx - 200), min(201, idx + 1))
smc_temp = SMCAnalyzer()
df_slice = smc_temp.calculate_all(df_slice)
signal = smc_temp.generate_signal(df_slice)
if signal and signal.signal_type in ["BUY", "SELL"] and signal.confidence >= confidence_threshold:
# Get ML prediction
ml_signal, ml_conf = ml_sim.predict(df, idx)
should_trade = False
if system_type == "SMC_ONLY":
# SMC-only: always trade on SMC signal
should_trade = True
elif system_type == "ML_SMC_GOLDEN":
if is_golden:
# Golden Time: require ML+SMC alignment
ml_agrees = (
(signal.signal_type == "BUY" and ml_signal == "BUY") or
(signal.signal_type == "SELL" and ml_signal == "SELL")
)
should_trade = ml_agrees and ml_conf >= 0.50
else:
# Non-golden: SMC-only with ML weak filter
ml_strongly_disagrees = (
(signal.signal_type == "BUY" and ml_signal == "SELL" and ml_conf > 0.65) or
(signal.signal_type == "SELL" and ml_signal == "BUY" and ml_conf > 0.65)
)
should_trade = not ml_strongly_disagrees
if should_trade:
position = {
"time": current_time,
"direction": signal.signal_type,
"entry": signal.entry_price,
"tp": signal.take_profit,
"conf": signal.confidence,
"session": session,
"is_golden": is_golden,
}
last_trade_idx = idx
# Progress
if idx % 10000 == 0:
print(f" Processing bar {idx}/{len(df)}...")
return trades
def calc_stats(trades: List[Trade], name: str, initial_capital: float) -> Dict:
"""Calculate statistics for trades."""
if not trades:
return {
"name": name, "trades": 0, "wins": 0, "losses": 0,
"win_rate": 0, "total_pnl": 0, "avg_win": 0, "avg_loss": 0,
"profit_factor": 0, "max_drawdown": 0, "avg_hold_hours": 0,
"final_capital": initial_capital,
}
wins = [t for t in trades if t.pnl > 0]
losses = [t for t in trades if t.pnl <= 0]
total_wins = sum(t.pnl for t in wins) if wins else 0
total_losses = abs(sum(t.pnl for t in losses)) if losses else 0
# Calculate drawdown
capital = initial_capital
peak = capital
max_dd = 0
for t in trades:
capital += t.pnl
peak = max(peak, capital)
dd = (peak - capital) / peak * 100
max_dd = max(max_dd, dd)
return {
"name": name,
"trades": len(trades),
"wins": len(wins),
"losses": len(losses),
"win_rate": 100 * len(wins) / len(trades) if trades else 0,
"total_pnl": sum(t.pnl for t in trades),
"avg_win": total_wins / len(wins) if wins else 0,
"avg_loss": total_losses / len(losses) if losses else 0,
"profit_factor": total_wins / total_losses if total_losses > 0 else float('inf'),
"max_drawdown": max_dd,
"avg_hold_hours": sum(t.hold_time_hours for t in trades) / len(trades) if trades else 0,
"final_capital": initial_capital + sum(t.pnl for t in trades),
}
def print_stats(stats: Dict):
"""Print statistics for a system."""
print(f"\n {stats['name']}:")
print(f" Total Trades: {stats['trades']}")
print(f" Win Rate: {stats['win_rate']:.1f}% ({stats['wins']}/{stats['losses']})")
print(f" Total P/L: ${stats['total_pnl']:.2f}")
print(f" Avg Win: ${stats['avg_win']:.2f}")
print(f" Avg Loss: ${stats['avg_loss']:.2f}")
print(f" Profit Factor: {stats['profit_factor']:.2f}")
print(f" Max Drawdown: {stats['max_drawdown']:.2f}%")
print(f" Avg Hold Time: {stats['avg_hold_hours']:.1f}h")
print(f" Final Capital: ${stats['final_capital']:.2f}")
if __name__ == "__main__":
run_comparison()