Files
xau-ai-trading-bot/src/news_agent.py
T
buckybonezandClaude Opus 4.5 538d3ae50b 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

565 lines
20 KiB
Python

"""
News Agent - Market Sentiment & Economic Calendar Analysis
==========================================================
Mengintegrasikan analisis berita untuk keputusan trading yang lebih cerdas.
Fitur:
1. MT5 Economic Calendar - Deteksi news high-impact (NFP, FOMC, CPI)
2. Keyword Sentiment Analysis - Analisis headline berita
3. News Filter Gatekeeper - Blokir trading saat kondisi berbahaya
Prinsip: "Sentimen-First, Technical-Second"
- Jika ada news high-impact -> STOP trading
- Jika sentimen sangat negatif -> Reduce position size
- Jika aman -> Proceed dengan analisis teknikal
"""
import os
from datetime import datetime, timedelta
from dataclasses import dataclass
from typing import List, Optional, Tuple
from enum import Enum
from loguru import logger
class MarketCondition(Enum):
"""Kondisi market berdasarkan news analysis."""
SAFE = "safe" # Aman untuk trading
CAUTION = "caution" # Hati-hati, reduce size
DANGER_NEWS = "danger_news" # Ada news high-impact, jangan trade
DANGER_SENTIMENT = "danger_sentiment" # Sentimen sangat negatif
UNKNOWN = "unknown" # Tidak bisa menentukan
@dataclass
class NewsEvent:
"""Representasi event dari economic calendar."""
name: str
currency: str
importance: int # 1=Low, 2=Medium, 3=High
time: datetime
actual: Optional[float] = None
forecast: Optional[float] = None
previous: Optional[float] = None
@dataclass
class SentimentResult:
"""Hasil analisis sentimen."""
score: float # -1.0 (bearish) to +1.0 (bullish)
label: str # BEARISH, NEUTRAL, BULLISH
confidence: float
keywords_found: List[str]
@dataclass
class NewsAnalysis:
"""Hasil lengkap analisis news."""
condition: MarketCondition
upcoming_events: List[NewsEvent]
sentiment: Optional[SentimentResult]
reason: str
can_trade: bool
recommended_lot_multiplier: float # 1.0 = normal, 0.5 = half, 0 = no trade
class NewsAgent:
"""
Agent untuk analisis berita dan economic calendar.
Berfungsi sebagai "Gatekeeper" sebelum trading:
1. Cek economic calendar MT5
2. Analisis sentimen dari headline
3. Tentukan apakah aman untuk trading
"""
# High-impact news keywords (USD-related for XAUUSD)
HIGH_IMPACT_EVENTS = [
"Non-Farm Payroll", "NFP", "FOMC", "Fed", "Federal Reserve",
"Interest Rate", "CPI", "Inflation", "GDP", "Unemployment",
"Powell", "Yellen", "Treasury", "Core PCE", "Retail Sales",
"ISM Manufacturing", "ISM Services", "PPI", "Trade Balance",
]
# Bearish keywords untuk gold
BEARISH_KEYWORDS = [
# Geopolitical - usually bullish for gold, but sudden de-escalation is bearish
"peace deal", "ceasefire", "de-escalation", "talks succeed",
# Economic - hawkish Fed is bearish for gold
"rate hike", "hawkish", "tightening", "strong dollar", "dollar surge",
"inflation falls", "inflation drops", "fed raises", "higher rates",
"economy strong", "jobs surge", "employment rises",
# Market sentiment
"risk on", "stocks rally", "equity surge", "sell gold", "gold crash",
"gold plunge", "gold drops", "gold falls", "bearish gold",
]
# Bullish keywords untuk gold
BULLISH_KEYWORDS = [
# Geopolitical - uncertainty is bullish for gold
"war", "conflict", "invasion", "attack", "missile", "escalation",
"tension", "crisis", "emergency", "pandemic", "outbreak",
# Economic - dovish Fed is bullish for gold
"rate cut", "dovish", "easing", "stimulus", "qe", "quantitative",
"recession", "slowdown", "weak economy", "jobs miss", "unemployment rises",
"inflation rises", "inflation surge", "fed pauses", "lower rates",
# Market sentiment
"risk off", "safe haven", "gold surge", "gold rally", "bullish gold",
"buy gold", "gold demand", "central bank buying",
]
# Neutral/cautionary keywords
VOLATILE_KEYWORDS = [
"breaking", "urgent", "flash", "sudden", "unexpected", "surprise",
"shock", "crash", "plunge", "spike", "surge", "volatility",
]
def __init__(
self,
news_buffer_minutes: int = 30,
high_impact_buffer_minutes: int = 60,
enable_mt5_calendar: bool = True,
enable_sentiment: bool = True,
):
"""
Initialize News Agent.
Args:
news_buffer_minutes: Jangan trade X menit sebelum/sesudah news biasa
high_impact_buffer_minutes: Jangan trade X menit sebelum/sesudah news high-impact
enable_mt5_calendar: Aktifkan pengecekan MT5 calendar
enable_sentiment: Aktifkan analisis sentimen
"""
self.news_buffer_minutes = news_buffer_minutes
self.high_impact_buffer_minutes = high_impact_buffer_minutes
self.enable_mt5_calendar = enable_mt5_calendar
self.enable_sentiment = enable_sentiment
# Cache untuk mengurangi API calls
self._calendar_cache: List[NewsEvent] = []
self._cache_time: Optional[datetime] = None
self._cache_duration = timedelta(minutes=15)
logger.info("News Agent initialized")
logger.info(f" News buffer: {news_buffer_minutes} minutes")
logger.info(f" High-impact buffer: {high_impact_buffer_minutes} minutes")
def check_economic_calendar(self) -> Tuple[MarketCondition, List[NewsEvent], str]:
"""
Cek MT5 Economic Calendar untuk news high-impact.
Returns:
(condition, events, reason)
"""
try:
import MetaTrader5 as mt5
# Check if MT5 is already initialized (by main connector)
# Don't call mt5.initialize() here as it conflicts with main connection
terminal_info = mt5.terminal_info()
if terminal_info is None:
# MT5 not initialized - skip silently (main connector will handle)
# Don't log warning to avoid spam
return MarketCondition.SAFE, [], "MT5 calendar check skipped"
now = datetime.now()
# Check high-impact window (60 min before/after)
hi_start = now - timedelta(minutes=self.high_impact_buffer_minutes)
hi_end = now + timedelta(minutes=self.high_impact_buffer_minutes)
# Check normal news window (30 min before/after)
news_start = now - timedelta(minutes=self.news_buffer_minutes)
news_end = now + timedelta(minutes=self.news_buffer_minutes)
# Get calendar events
# Note: MT5 calendar functions may vary by broker
# Using a broader approach
try:
# Try to get calendar events (broker-dependent)
# Some brokers don't expose this API
events = mt5.copy_ticks_from("XAUUSD", now - timedelta(hours=1), 1, mt5.COPY_TICKS_INFO)
# If we get here, try calendar
calendar_events = []
# Fallback: Check known high-impact times
# NFP: First Friday of month, 8:30 AM ET (20:30 WIB)
# FOMC: ~8 times per year, 2:00 PM ET (02:00 WIB next day)
# CPI: Monthly, 8:30 AM ET
high_impact_found = self._check_known_events(now)
if high_impact_found:
return MarketCondition.DANGER_NEWS, [], high_impact_found
except Exception as e:
logger.debug(f"Calendar API not available: {e}")
return MarketCondition.SAFE, [], "No high-impact news detected"
except ImportError:
logger.warning("MT5 not available for calendar check")
return MarketCondition.UNKNOWN, [], "MT5 module not available"
except Exception as e:
logger.error(f"Error checking calendar: {e}")
return MarketCondition.UNKNOWN, [], str(e)
def _check_known_events(self, now: datetime) -> Optional[str]:
"""
Check for known high-impact events based on schedule.
AGGRESSIVE MODE: Only block for HIGH impact news (NFP, FOMC, CPI)
Based on backtest: +/-1h HIGH only gives best results
Returns:
Event name if within danger zone, None otherwise
"""
weekday = now.weekday() # 0=Monday, 4=Friday
day = now.day
hour = now.hour
# NFP: First Friday of month, 20:30 WIB (8:30 AM ET)
# Block: 19:30-21:30 WIB (+/-1h)
if weekday == 4 and day <= 7:
# First Friday
if 19 <= hour <= 21:
return "NFP (Non-Farm Payroll) - HIGH IMPACT"
# FOMC: ~8 times per year, 02:00 WIB (2:00 PM ET previous day)
# Only check on typical FOMC weeks (specific dates)
# FOMC 2025-2026 dates roughly: Jan 29, Mar 19, May 7, Jun 18, Jul 30, Sep 17, Nov 5, Dec 17
fomc_dates = [
(1, 29), (3, 19), (5, 7), (6, 18), (7, 30), (9, 17), (11, 5), (12, 17), # 2025
(1, 29), (3, 18), (5, 6), (6, 17), (7, 29), # 2026
]
current_month_day = (now.month, now.day)
for fomc_month, fomc_day in fomc_dates:
if current_month_day == (fomc_month, fomc_day):
if 1 <= hour <= 3: # FOMC announcement ~02:00 WIB
return "FOMC Decision - HIGH IMPACT"
# CPI: Monthly around 10th-15th, 20:30 WIB (8:30 AM ET)
# Only block the exact release window, not entire day
# CPI is HIGH impact for gold
if 10 <= day <= 15 and 19 <= hour <= 21:
# Check if it looks like CPI day (usually Tuesday/Wednesday)
if weekday in [1, 2, 3]: # Tuesday, Wednesday, Thursday
return "CPI (Inflation) - HIGH IMPACT"
return None
def analyze_sentiment(self, headlines: List[str]) -> SentimentResult:
"""
Analisis sentimen dari headline berita.
Args:
headlines: List of news headlines
Returns:
SentimentResult dengan score dan label
"""
if not headlines:
return SentimentResult(
score=0.0,
label="NEUTRAL",
confidence=0.0,
keywords_found=[],
)
# Combine headlines
text = " ".join(headlines).lower()
# Count keyword matches
bearish_matches = []
bullish_matches = []
volatile_matches = []
for keyword in self.BEARISH_KEYWORDS:
if keyword.lower() in text:
bearish_matches.append(keyword)
for keyword in self.BULLISH_KEYWORDS:
if keyword.lower() in text:
bullish_matches.append(keyword)
for keyword in self.VOLATILE_KEYWORDS:
if keyword.lower() in text:
volatile_matches.append(keyword)
# Calculate score
bullish_score = len(bullish_matches) * 0.3
bearish_score = len(bearish_matches) * 0.3
volatile_penalty = len(volatile_matches) * 0.1
# Net score: positive = bullish, negative = bearish
net_score = bullish_score - bearish_score
# Clamp to [-1, 1]
net_score = max(-1.0, min(1.0, net_score))
# Determine label
if net_score > 0.3:
label = "BULLISH"
elif net_score < -0.3:
label = "BEARISH"
else:
label = "NEUTRAL"
# Confidence based on keyword matches
total_matches = len(bearish_matches) + len(bullish_matches)
confidence = min(1.0, total_matches * 0.2) if total_matches > 0 else 0.0
# Reduce confidence if volatile keywords found (uncertain situation)
if volatile_matches:
confidence *= 0.7
all_keywords = bearish_matches + bullish_matches + volatile_matches
return SentimentResult(
score=net_score,
label=label,
confidence=confidence,
keywords_found=all_keywords,
)
def analyze(
self,
headlines: Optional[List[str]] = None,
check_calendar: bool = True,
) -> NewsAnalysis:
"""
Analisis lengkap news untuk keputusan trading.
Args:
headlines: Optional list of news headlines
check_calendar: Whether to check economic calendar
Returns:
NewsAnalysis dengan rekomendasi trading
"""
condition = MarketCondition.SAFE
events: List[NewsEvent] = []
sentiment: Optional[SentimentResult] = None
reasons = []
lot_multiplier = 1.0
# 1. Check Economic Calendar
if check_calendar and self.enable_mt5_calendar:
cal_condition, cal_events, cal_reason = self.check_economic_calendar()
events = cal_events
if cal_condition == MarketCondition.DANGER_NEWS:
condition = MarketCondition.DANGER_NEWS
reasons.append(f"High-impact news: {cal_reason}")
lot_multiplier = 0.0 # No trading
elif cal_condition == MarketCondition.CAUTION:
reasons.append(f"News caution: {cal_reason}")
lot_multiplier = 0.5 # Half size
# 2. Analyze Sentiment (if headlines provided)
if headlines and self.enable_sentiment:
sentiment = self.analyze_sentiment(headlines)
if sentiment.label == "BEARISH" and sentiment.confidence > 0.5:
if condition != MarketCondition.DANGER_NEWS:
condition = MarketCondition.DANGER_SENTIMENT
reasons.append(f"Bearish sentiment: {sentiment.keywords_found}")
lot_multiplier = min(lot_multiplier, 0.5)
elif sentiment.label == "BULLISH" and sentiment.confidence > 0.5:
reasons.append(f"Bullish sentiment: {sentiment.keywords_found}")
# Could increase multiplier, but safer to keep at 1.0
# Determine if can trade
can_trade = condition in [MarketCondition.SAFE, MarketCondition.CAUTION]
# Build reason string
if not reasons:
reasons.append("Market conditions normal")
reason_str = "; ".join(reasons)
return NewsAnalysis(
condition=condition,
upcoming_events=events,
sentiment=sentiment,
reason=reason_str,
can_trade=can_trade,
recommended_lot_multiplier=lot_multiplier,
)
def should_trade(self, headlines: Optional[List[str]] = None) -> Tuple[bool, str, float]:
"""
Quick check: Apakah aman untuk trading?
Returns:
(can_trade, reason, lot_multiplier)
"""
analysis = self.analyze(headlines=headlines)
return analysis.can_trade, analysis.reason, analysis.recommended_lot_multiplier
def get_status_summary(self) -> str:
"""Get human-readable status summary."""
analysis = self.analyze()
status = f"News Status: {analysis.condition.value.upper()}\n"
status += f"Can Trade: {'Yes' if analysis.can_trade else 'NO'}\n"
status += f"Lot Multiplier: {analysis.recommended_lot_multiplier:.1f}x\n"
status += f"Reason: {analysis.reason}"
return status
def create_news_agent(
news_buffer_minutes: int = 30,
high_impact_buffer_minutes: int = 60,
) -> NewsAgent:
"""Factory function untuk membuat NewsAgent."""
return NewsAgent(
news_buffer_minutes=news_buffer_minutes,
high_impact_buffer_minutes=high_impact_buffer_minutes,
)
# ============================================================
# EXTERNAL NEWS API INTEGRATION (Optional - for future use)
# ============================================================
class ExternalNewsProvider:
"""
Base class untuk external news providers.
Implement untuk NewsAPI, ForexFactory, Bloomberg, dll.
"""
def get_headlines(self, keywords: List[str] = None) -> List[str]:
"""Get latest headlines. Override in subclass."""
raise NotImplementedError
def get_gold_news(self) -> List[str]:
"""Get gold-specific news."""
return self.get_headlines(["gold", "XAUUSD", "precious metals"])
class NewsAPIProvider(ExternalNewsProvider):
"""
NewsAPI.org integration.
Requires API key from https://newsapi.org/
"""
def __init__(self, api_key: str):
self.api_key = api_key
self.base_url = "https://newsapi.org/v2"
def get_headlines(self, keywords: List[str] = None) -> List[str]:
"""Fetch headlines from NewsAPI."""
try:
import requests
query = " OR ".join(keywords) if keywords else "gold forex"
response = requests.get(
f"{self.base_url}/everything",
params={
"q": query,
"apiKey": self.api_key,
"language": "en",
"sortBy": "publishedAt",
"pageSize": 10,
},
timeout=10,
)
if response.status_code == 200:
data = response.json()
return [article["title"] for article in data.get("articles", [])]
else:
logger.warning(f"NewsAPI error: {response.status_code}")
return []
except Exception as e:
logger.error(f"Error fetching from NewsAPI: {e}")
return []
class ForexFactoryProvider(ExternalNewsProvider):
"""
ForexFactory calendar scraper.
Note: Scraping may violate ToS, use responsibly.
"""
def get_headlines(self, keywords: List[str] = None) -> List[str]:
"""ForexFactory doesn't provide headlines, only calendar."""
return []
def get_calendar_events(self) -> List[dict]:
"""
Scrape ForexFactory calendar.
Returns list of events with impact level.
"""
# Implementation would require web scraping
# For now, return empty (use MT5 calendar instead)
logger.info("ForexFactory scraping not implemented - use MT5 calendar")
return []
# ============================================================
# TEST
# ============================================================
if __name__ == "__main__":
# Test News Agent
agent = create_news_agent()
print("=" * 60)
print("NEWS AGENT TEST")
print("=" * 60)
# Test without headlines
print("\n1. Check without headlines:")
can_trade, reason, multiplier = agent.should_trade()
print(f" Can Trade: {can_trade}")
print(f" Reason: {reason}")
print(f" Lot Multiplier: {multiplier}x")
# Test with bearish headlines
print("\n2. Test with BEARISH headlines:")
bearish_headlines = [
"Fed signals rate hike likely next month",
"Dollar surges as inflation falls below expectations",
"Gold plunges on hawkish Fed comments",
]
sentiment = agent.analyze_sentiment(bearish_headlines)
print(f" Score: {sentiment.score:.2f}")
print(f" Label: {sentiment.label}")
print(f" Keywords: {sentiment.keywords_found}")
can_trade, reason, multiplier = agent.should_trade(bearish_headlines)
print(f" Can Trade: {can_trade}")
print(f" Lot Multiplier: {multiplier}x")
# Test with bullish headlines
print("\n3. Test with BULLISH headlines:")
bullish_headlines = [
"War tensions escalate in Middle East",
"Fed signals potential rate cut next quarter",
"Gold surges as safe haven demand increases",
"Central banks buying gold at record pace",
]
sentiment = agent.analyze_sentiment(bullish_headlines)
print(f" Score: {sentiment.score:.2f}")
print(f" Label: {sentiment.label}")
print(f" Keywords: {sentiment.keywords_found}")
can_trade, reason, multiplier = agent.should_trade(bullish_headlines)
print(f" Can Trade: {can_trade}")
print(f" Lot Multiplier: {multiplier}x")
# Test full analysis
print("\n4. Full Analysis:")
analysis = agent.analyze(headlines=bullish_headlines)
print(f" Condition: {analysis.condition.value}")
print(f" Can Trade: {analysis.can_trade}")
print(f" Reason: {analysis.reason}")
print("\n" + "=" * 60)
print("Status Summary:")
print("=" * 60)
print(agent.get_status_summary())