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

809 lines
26 KiB
Python

"""
Walk-Forward Backtest with News Filter
========================================
Backtest 1 tahun dengan simulasi news filter (NFP, FOMC, CPI).
Fitur:
1. Historical news calendar (actual dates dari 2025)
2. Skip trading saat high-impact news
3. Compare: WITH news filter vs WITHOUT
"""
import polars as pl
import numpy as np
from datetime import datetime, timedelta, date
from dataclasses import dataclass, field
from typing import List, Dict, Optional, Tuple
from pathlib import Path
import pickle
from loguru import logger
import sys
# Configure logging
logger.remove()
logger.add(sys.stdout, format="<green>{time:HH:mm:ss}</green> | <level>{level:<8}</level> | <cyan>{message}</cyan>", level="INFO")
# ============================================================
# HISTORICAL NEWS CALENDAR 2025
# ============================================================
# Actual high-impact news dates for USD (affects XAUUSD)
# Format: (date, event_name, impact_level)
HISTORICAL_NEWS_2025 = [
# January 2025
(date(2025, 1, 3), "NFP", "HIGH"),
(date(2025, 1, 14), "CPI", "HIGH"),
(date(2025, 1, 15), "PPI", "MEDIUM"),
(date(2025, 1, 29), "FOMC", "HIGH"),
(date(2025, 1, 30), "GDP Q4", "HIGH"),
# February 2025
(date(2025, 2, 7), "NFP", "HIGH"),
(date(2025, 2, 12), "CPI", "HIGH"),
(date(2025, 2, 13), "PPI", "MEDIUM"),
(date(2025, 2, 27), "GDP Revision", "MEDIUM"),
# March 2025
(date(2025, 3, 7), "NFP", "HIGH"),
(date(2025, 3, 12), "CPI", "HIGH"),
(date(2025, 3, 13), "PPI", "MEDIUM"),
(date(2025, 3, 19), "FOMC", "HIGH"),
(date(2025, 3, 27), "GDP Final", "MEDIUM"),
# April 2025
(date(2025, 4, 4), "NFP", "HIGH"),
(date(2025, 4, 10), "CPI", "HIGH"),
(date(2025, 4, 11), "PPI", "MEDIUM"),
(date(2025, 4, 30), "GDP Q1", "HIGH"),
# May 2025
(date(2025, 5, 2), "NFP", "HIGH"),
(date(2025, 5, 7), "FOMC", "HIGH"),
(date(2025, 5, 13), "CPI", "HIGH"),
(date(2025, 5, 14), "PPI", "MEDIUM"),
(date(2025, 5, 29), "GDP Revision", "MEDIUM"),
# June 2025
(date(2025, 6, 6), "NFP", "HIGH"),
(date(2025, 6, 11), "CPI", "HIGH"),
(date(2025, 6, 12), "PPI", "MEDIUM"),
(date(2025, 6, 18), "FOMC", "HIGH"),
(date(2025, 6, 26), "GDP Final", "MEDIUM"),
# July 2025
(date(2025, 7, 3), "NFP", "HIGH"),
(date(2025, 7, 11), "CPI", "HIGH"),
(date(2025, 7, 15), "PPI", "MEDIUM"),
(date(2025, 7, 30), "FOMC", "HIGH"),
(date(2025, 7, 31), "GDP Q2", "HIGH"),
# August 2025
(date(2025, 8, 1), "NFP", "HIGH"),
(date(2025, 8, 13), "CPI", "HIGH"),
(date(2025, 8, 14), "PPI", "MEDIUM"),
(date(2025, 8, 28), "GDP Revision", "MEDIUM"),
# September 2025
(date(2025, 9, 5), "NFP", "HIGH"),
(date(2025, 9, 10), "CPI", "HIGH"),
(date(2025, 9, 11), "PPI", "MEDIUM"),
(date(2025, 9, 17), "FOMC", "HIGH"),
(date(2025, 9, 25), "GDP Final", "MEDIUM"),
# October 2025
(date(2025, 10, 3), "NFP", "HIGH"),
(date(2025, 10, 10), "CPI", "HIGH"),
(date(2025, 10, 14), "PPI", "MEDIUM"),
(date(2025, 10, 30), "GDP Q3", "HIGH"),
# November 2025
(date(2025, 11, 7), "NFP", "HIGH"),
(date(2025, 11, 5), "FOMC", "HIGH"),
(date(2025, 11, 13), "CPI", "HIGH"),
(date(2025, 11, 14), "PPI", "MEDIUM"),
(date(2025, 11, 26), "GDP Revision", "MEDIUM"),
# December 2025
(date(2025, 12, 5), "NFP", "HIGH"),
(date(2025, 12, 10), "CPI", "HIGH"),
(date(2025, 12, 11), "PPI", "MEDIUM"),
(date(2025, 12, 17), "FOMC", "HIGH"),
# January 2026
(date(2026, 1, 10), "NFP", "HIGH"),
(date(2026, 1, 15), "CPI", "HIGH"),
(date(2026, 1, 29), "FOMC", "HIGH"),
# February 2026
(date(2026, 2, 5), "NFP", "HIGH"),
]
@dataclass
class NewsFilter:
"""News filter untuk backtest."""
# Buffer hours sebelum dan sesudah news
high_impact_buffer_hours: int = 2
medium_impact_buffer_hours: int = 1
def __post_init__(self):
# Build lookup dict for fast checking
self.news_dates = {}
for news_date, event_name, impact in HISTORICAL_NEWS_2025:
if news_date not in self.news_dates:
self.news_dates[news_date] = []
self.news_dates[news_date].append((event_name, impact))
def is_news_blocked(self, dt: datetime) -> Tuple[bool, str]:
"""
Check if trading should be blocked due to news.
Returns:
(is_blocked, reason)
"""
current_date = dt.date()
# Check current day
if current_date in self.news_dates:
for event_name, impact in self.news_dates[current_date]:
if impact == "HIGH":
# Block entire day for HIGH impact news
return True, f"{event_name} (HIGH)"
elif impact == "MEDIUM":
# Block around typical release time (14:30-16:00 WIB typical)
if 14 <= dt.hour <= 16:
return True, f"{event_name} (MEDIUM)"
# Check day before (for overnight positions)
prev_date = current_date - timedelta(days=1)
if prev_date in self.news_dates:
for event_name, impact in self.news_dates[prev_date]:
if impact == "HIGH" and dt.hour < 6:
return True, f"{event_name} aftermath"
return False, "Clear"
@dataclass
class BacktestConfig:
"""Configuration for backtest."""
start_date: date = date(2025, 5, 22) # Adjusted based on available data
end_date: date = date(2026, 2, 5)
initial_capital: float = 5000.0
lot_size: float = 0.02
# ML thresholds (from previous optimization)
ml_threshold: float = 0.65
ml_only_threshold: float = 0.70
# Risk settings
max_daily_loss_pct: float = 0.02
sl_atr_mult: float = 1.5
tp_atr_mult: float = 3.0
@dataclass
class Trade:
"""Single trade record."""
entry_time: datetime
exit_time: datetime
direction: str
entry_price: float
exit_price: float
lot_size: float
pnl: float
ml_confidence: float
news_event: str = ""
@dataclass
class BacktestResult:
"""Backtest result summary."""
total_trades: int
winning_trades: int
losing_trades: int
win_rate: float
total_pnl: float
avg_win: float
avg_loss: float
profit_factor: float
max_drawdown: float
trades: List[Trade] = field(default_factory=list)
# News-specific stats
trades_blocked_by_news: int = 0
news_events_avoided: List[str] = field(default_factory=list)
def load_historical_data(symbol: str = "XAUUSD") -> Optional[pl.DataFrame]:
"""Load historical market data."""
try:
import MetaTrader5 as mt5
from src.config import get_config
config = get_config()
# Initialize with full config
if not mt5.initialize(
path=config.mt5_path,
login=config.mt5_login,
password=config.mt5_password,
server=config.mt5_server,
):
logger.error(f"MT5 initialization failed: {mt5.last_error()}")
return None
logger.info(f"MT5 connected: {mt5.account_info().server}")
# Enable symbol
mt5.symbol_select(symbol, True)
import time
time.sleep(0.5) # Wait for symbol to be ready
# Get available M5 data (use last N bars instead of date range)
# MT5 demo accounts typically have limited history
# Get 60,000 bars (~200 days of M5 data)
rates = mt5.copy_rates_from_pos(symbol, mt5.TIMEFRAME_M5, 0, 60000)
if rates is None or len(rates) == 0:
logger.error(f"No data received from MT5: {mt5.last_error()}")
# Try alternative method with smaller batch
rates = mt5.copy_rates_from(symbol, mt5.TIMEFRAME_M5, datetime.now(), 50000)
if rates is None or len(rates) == 0:
logger.error(f"Still no data: {mt5.last_error()}")
mt5.shutdown()
return None
logger.info(f"Received {len(rates)} bars")
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],
})
logger.info(f"Loaded {len(df)} bars from {df['time'].min()} to {df['time'].max()}")
return df
except Exception as e:
logger.error(f"Error loading data: {e}")
return None
def calculate_features(df: pl.DataFrame) -> pl.DataFrame:
"""Calculate technical features for ML prediction."""
# ATR
df = df.with_columns([
(pl.col("high") - pl.col("low")).alias("tr1"),
(pl.col("high") - pl.col("close").shift(1)).abs().alias("tr2"),
(pl.col("low") - pl.col("close").shift(1)).abs().alias("tr3"),
])
df = df.with_columns([
pl.max_horizontal("tr1", "tr2", "tr3").alias("tr")
])
df = df.with_columns([
pl.col("tr").rolling_mean(window_size=14).alias("atr_14")
])
# RSI
df = df.with_columns([
(pl.col("close") - pl.col("close").shift(1)).alias("change")
])
df = df.with_columns([
pl.when(pl.col("change") > 0).then(pl.col("change")).otherwise(0).alias("gain"),
pl.when(pl.col("change") < 0).then(pl.col("change").abs()).otherwise(0).alias("loss"),
])
df = df.with_columns([
pl.col("gain").rolling_mean(window_size=14).alias("avg_gain"),
pl.col("loss").rolling_mean(window_size=14).alias("avg_loss"),
])
df = df.with_columns([
(100 - (100 / (1 + pl.col("avg_gain") / (pl.col("avg_loss") + 1e-10)))).alias("rsi_14")
])
# Moving Averages
df = df.with_columns([
pl.col("close").rolling_mean(window_size=20).alias("sma_20"),
pl.col("close").rolling_mean(window_size=50).alias("sma_50"),
pl.col("close").ewm_mean(span=12).alias("ema_12"),
pl.col("close").ewm_mean(span=26).alias("ema_26"),
])
# MACD
df = df.with_columns([
(pl.col("ema_12") - pl.col("ema_26")).alias("macd")
])
df = df.with_columns([
pl.col("macd").ewm_mean(span=9).alias("macd_signal")
])
# Bollinger Bands
df = df.with_columns([
pl.col("close").rolling_std(window_size=20).alias("bb_std")
])
df = df.with_columns([
(pl.col("sma_20") + 2 * pl.col("bb_std")).alias("bb_upper"),
(pl.col("sma_20") - 2 * pl.col("bb_std")).alias("bb_lower"),
])
# Momentum features
df = df.with_columns([
((pl.col("close") - pl.col("close").shift(5)) / pl.col("close").shift(5) * 100).alias("momentum_5"),
((pl.col("close") - pl.col("close").shift(10)) / pl.col("close").shift(10) * 100).alias("momentum_10"),
((pl.col("close") - pl.col("sma_20")) / pl.col("sma_20") * 100).alias("price_to_sma"),
])
# Volatility
df = df.with_columns([
(pl.col("atr_14") / pl.col("close") * 100).alias("volatility_pct")
])
# Hour and day features
df = df.with_columns([
pl.col("time").dt.hour().alias("hour"),
pl.col("time").dt.weekday().alias("dayofweek"),
])
return df.drop_nulls()
def simulate_ml_prediction(df: pl.DataFrame, idx: int) -> Tuple[str, float]:
"""
Simulate ML prediction based on technical indicators.
Returns (signal, confidence).
"""
row = df.row(idx, named=True)
# Score based on multiple factors
score = 0.5 # Neutral base
# RSI
rsi = row.get("rsi_14", 50)
if rsi < 30:
score += 0.15 # Oversold - bullish
elif rsi > 70:
score -= 0.15 # Overbought - bearish
# MACD
macd = row.get("macd", 0)
macd_signal = row.get("macd_signal", 0)
if macd > macd_signal:
score += 0.1
else:
score -= 0.1
# Price vs SMA
close = row.get("close", 0)
sma_20 = row.get("sma_20", close)
sma_50 = row.get("sma_50", close)
if close > sma_20 > sma_50:
score += 0.1 # Bullish trend
elif close < sma_20 < sma_50:
score -= 0.1 # Bearish trend
# Bollinger Bands
bb_upper = row.get("bb_upper", close + 10)
bb_lower = row.get("bb_lower", close - 10)
if close < bb_lower:
score += 0.1 # Oversold
elif close > bb_upper:
score -= 0.1 # Overbought
# Momentum
momentum = row.get("momentum_5", 0)
if momentum > 0.5:
score += 0.05
elif momentum < -0.5:
score -= 0.05
# Add some randomness to simulate real ML variance
noise = np.random.normal(0, 0.1)
score = max(0, min(1, score + noise))
# Determine signal and confidence
if score > 0.5:
signal = "BUY"
confidence = 0.5 + (score - 0.5) * 0.8 # Scale to 0.5-0.9
else:
signal = "SELL"
confidence = 0.5 + (0.5 - score) * 0.8
return signal, confidence
def run_backtest(
df: pl.DataFrame,
config: BacktestConfig,
use_news_filter: bool = True,
) -> BacktestResult:
"""
Run backtest with or without news filter.
"""
news_filter = NewsFilter() if use_news_filter else None
trades: List[Trade] = []
trades_blocked = 0
news_avoided = []
capital = config.initial_capital
daily_pnl = 0.0
current_date = None
position = None # {"direction": str, "entry_price": float, "entry_time": datetime, "sl": float, "tp": float, "confidence": float}
logger.info(f"Starting backtest ({'WITH' if use_news_filter else 'WITHOUT'} news filter)")
logger.info(f"Period: {config.start_date} to {config.end_date}")
for idx in range(100, len(df)): # Start after warmup
row = df.row(idx, named=True)
current_time = row["time"]
# Filter by date range
if current_time.date() < config.start_date:
continue
if current_time.date() > config.end_date:
break
# Daily reset
if current_date != current_time.date():
current_date = current_time.date()
daily_pnl = 0.0
# Check daily loss limit
if daily_pnl < -config.max_daily_loss_pct * capital:
continue
# Get current price
close = row["close"]
high = row["high"]
low = row["low"]
atr = row.get("atr_14", close * 0.003)
# Manage existing position
if position is not None:
# Check SL/TP
if position["direction"] == "BUY":
if low <= position["sl"]:
# Stop loss hit
pnl = (position["sl"] - position["entry_price"]) * config.lot_size * 100
trades.append(Trade(
entry_time=position["entry_time"],
exit_time=current_time,
direction="BUY",
entry_price=position["entry_price"],
exit_price=position["sl"],
lot_size=config.lot_size,
pnl=pnl,
ml_confidence=position["confidence"],
))
daily_pnl += pnl
capital += pnl
position = None
elif high >= position["tp"]:
# Take profit hit
pnl = (position["tp"] - position["entry_price"]) * config.lot_size * 100
trades.append(Trade(
entry_time=position["entry_time"],
exit_time=current_time,
direction="BUY",
entry_price=position["entry_price"],
exit_price=position["tp"],
lot_size=config.lot_size,
pnl=pnl,
ml_confidence=position["confidence"],
))
daily_pnl += pnl
capital += pnl
position = None
else: # SELL
if high >= position["sl"]:
# Stop loss hit
pnl = (position["entry_price"] - position["sl"]) * config.lot_size * 100
trades.append(Trade(
entry_time=position["entry_time"],
exit_time=current_time,
direction="SELL",
entry_price=position["entry_price"],
exit_price=position["sl"],
lot_size=config.lot_size,
pnl=pnl,
ml_confidence=position["confidence"],
))
daily_pnl += pnl
capital += pnl
position = None
elif low <= position["tp"]:
# Take profit hit
pnl = (position["entry_price"] - position["tp"]) * config.lot_size * 100
trades.append(Trade(
entry_time=position["entry_time"],
exit_time=current_time,
direction="SELL",
entry_price=position["entry_price"],
exit_price=position["tp"],
lot_size=config.lot_size,
pnl=pnl,
ml_confidence=position["confidence"],
))
daily_pnl += pnl
capital += pnl
position = None
# Skip if already in position
if position is not None:
continue
# NEWS FILTER CHECK
if news_filter is not None:
is_blocked, news_reason = news_filter.is_news_blocked(current_time)
if is_blocked:
trades_blocked += 1
if news_reason not in news_avoided:
news_avoided.append(news_reason)
continue
# Session filter (simplified - only trade during London/NY)
hour = current_time.hour
if hour < 14 or hour > 23: # WIB timezone
continue
# Get ML prediction
signal, confidence = simulate_ml_prediction(df, idx)
# Check confidence threshold
if confidence < config.ml_only_threshold:
continue
# Entry signal
if signal == "BUY":
sl = close - (atr * config.sl_atr_mult)
tp = close + (atr * config.tp_atr_mult)
position = {
"direction": "BUY",
"entry_price": close,
"entry_time": current_time,
"sl": sl,
"tp": tp,
"confidence": confidence,
}
else:
sl = close + (atr * config.sl_atr_mult)
tp = close - (atr * config.tp_atr_mult)
position = {
"direction": "SELL",
"entry_price": close,
"entry_time": current_time,
"sl": sl,
"tp": tp,
"confidence": confidence,
}
# Close any remaining position
if position is not None and len(df) > 0:
last_row = df.row(-1, named=True)
last_close = last_row["close"]
if position["direction"] == "BUY":
pnl = (last_close - position["entry_price"]) * config.lot_size * 100
else:
pnl = (position["entry_price"] - last_close) * config.lot_size * 100
trades.append(Trade(
entry_time=position["entry_time"],
exit_time=last_row["time"],
direction=position["direction"],
entry_price=position["entry_price"],
exit_price=last_close,
lot_size=config.lot_size,
pnl=pnl,
ml_confidence=position["confidence"],
))
# Calculate results
total_trades = len(trades)
winning_trades = sum(1 for t in trades if t.pnl > 0)
losing_trades = sum(1 for t in trades if t.pnl <= 0)
total_pnl = sum(t.pnl for t in trades)
wins = [t.pnl for t in trades if t.pnl > 0]
losses = [abs(t.pnl) for t in trades if t.pnl <= 0]
avg_win = np.mean(wins) if wins else 0
avg_loss = np.mean(losses) if losses else 0
total_wins = sum(wins) if wins else 0
total_losses = sum(losses) if losses else 1
profit_factor = total_wins / total_losses if total_losses > 0 else 0
# Calculate max drawdown
equity_curve = [config.initial_capital]
for t in trades:
equity_curve.append(equity_curve[-1] + t.pnl)
peak = equity_curve[0]
max_dd = 0
for equity in equity_curve:
if equity > peak:
peak = equity
dd = (peak - equity) / peak * 100
if dd > max_dd:
max_dd = dd
return BacktestResult(
total_trades=total_trades,
winning_trades=winning_trades,
losing_trades=losing_trades,
win_rate=winning_trades / total_trades * 100 if total_trades > 0 else 0,
total_pnl=total_pnl,
avg_win=avg_win,
avg_loss=avg_loss,
profit_factor=profit_factor,
max_drawdown=max_dd,
trades=trades,
trades_blocked_by_news=trades_blocked,
news_events_avoided=news_avoided,
)
def main():
"""Run comparison backtest."""
print("=" * 70)
print("WALK-FORWARD BACKTEST WITH NEWS FILTER")
print("=" * 70)
print()
# Load data
logger.info("Loading historical data...")
df = load_historical_data()
if df is None:
logger.error("Failed to load data")
return
# Calculate features
logger.info("Calculating features...")
df = calculate_features(df)
logger.info(f"Data ready: {len(df)} bars with features")
# Configuration
config = BacktestConfig(
start_date=date(2025, 5, 22), # Based on available MT5 data
end_date=date(2026, 2, 5),
initial_capital=5000.0,
lot_size=0.02,
ml_threshold=0.65,
ml_only_threshold=0.70,
)
print()
print("=" * 70)
print("BACKTEST 1: WITHOUT NEWS FILTER")
print("=" * 70)
result_no_news = run_backtest(df, config, use_news_filter=False)
print(f"""
Results WITHOUT News Filter:
-----------------------------
Total Trades : {result_no_news.total_trades}
Win Rate : {result_no_news.win_rate:.1f}%
Total P/L : ${result_no_news.total_pnl:,.2f}
Avg Win : ${result_no_news.avg_win:.2f}
Avg Loss : ${result_no_news.avg_loss:.2f}
Profit Factor : {result_no_news.profit_factor:.2f}
Max Drawdown : {result_no_news.max_drawdown:.1f}%
""")
print()
print("=" * 70)
print("BACKTEST 2: WITH NEWS FILTER")
print("=" * 70)
result_with_news = run_backtest(df, config, use_news_filter=True)
print(f"""
Results WITH News Filter:
-----------------------------
Total Trades : {result_with_news.total_trades}
Win Rate : {result_with_news.win_rate:.1f}%
Total P/L : ${result_with_news.total_pnl:,.2f}
Avg Win : ${result_with_news.avg_win:.2f}
Avg Loss : ${result_with_news.avg_loss:.2f}
Profit Factor : {result_with_news.profit_factor:.2f}
Max Drawdown : {result_with_news.max_drawdown:.1f}%
News Filter Stats:
-----------------------------
Trades Blocked : {result_with_news.trades_blocked_by_news}
Events Avoided : {len(result_with_news.news_events_avoided)}
""")
# Print avoided events
if result_with_news.news_events_avoided:
print("News Events Avoided:")
for event in result_with_news.news_events_avoided[:20]:
print(f" - {event}")
print()
print("=" * 70)
print("COMPARISON SUMMARY")
print("=" * 70)
# Calculate improvement
if result_no_news.total_pnl != 0:
pnl_improvement = ((result_with_news.total_pnl - result_no_news.total_pnl) / abs(result_no_news.total_pnl)) * 100
else:
pnl_improvement = 0
wr_improvement = result_with_news.win_rate - result_no_news.win_rate
dd_improvement = result_no_news.max_drawdown - result_with_news.max_drawdown
print(f"""
Without News With News Improvement
------------ --------- -----------
Total Trades {result_no_news.total_trades:<15} {result_with_news.total_trades:<13} {result_with_news.total_trades - result_no_news.total_trades:+d}
Win Rate {result_no_news.win_rate:<15.1f} {result_with_news.win_rate:<13.1f} {wr_improvement:+.1f}%
Total P/L ${result_no_news.total_pnl:<14,.2f} ${result_with_news.total_pnl:<12,.2f} {pnl_improvement:+.1f}%
Profit Factor {result_no_news.profit_factor:<15.2f} {result_with_news.profit_factor:<13.2f}
Max Drawdown {result_no_news.max_drawdown:<15.1f}% {result_with_news.max_drawdown:<12.1f}% {dd_improvement:+.1f}%
""")
# Verdict
print("=" * 70)
print("VERDICT")
print("=" * 70)
if result_with_news.win_rate > result_no_news.win_rate and result_with_news.total_pnl > result_no_news.total_pnl:
print("""
✅ NEWS FILTER RECOMMENDED
Alasan:
1. Win Rate meningkat
2. Total Profit meningkat
3. Menghindari volatilitas tinggi saat high-impact news
Dengan menghindari trading saat NFP, FOMC, CPI, bot menghindari
pergerakan tidak terduga yang sering merugikan.
""")
elif result_with_news.win_rate > result_no_news.win_rate:
print("""
⚠️ NEWS FILTER BERGUNA untuk Win Rate
Alasan:
- Win Rate meningkat (lebih sedikit loss dari news spike)
- Tapi total trades berkurang signifikan
- Pertimbangkan risk tolerance Anda
""")
elif result_with_news.max_drawdown < result_no_news.max_drawdown:
print("""
[!] NEWS FILTER BERGUNA untuk Risk Management
Alasan:
- Max Drawdown berkurang
- Menghindari loss besar saat news
- Trade lebih aman walau profit mungkin berkurang
""")
else:
print("""
❌ NEWS FILTER KURANG BERDAMPAK dalam backtest ini
Catatan:
- Backtest menggunakan simulated ML, bukan model asli
- Real-world impact mungkin berbeda
- High-impact news tetap berisiko tinggi
""")
print()
print("=" * 70)
print("Backtest completed!")
print("=" * 70)
if __name__ == "__main__":
main()