379 lines
11 KiB
Python
379 lines
11 KiB
Python
#!/usr/bin/env python3
|
|||
|
|
"""
|
||
|
|
📰 NEWS FILTER - Economic Calendar Integration
|
||
|
|
Blockiert Trading vor/nach High-Impact News Events
|
||
|
|
|
||
|
|
FEATURES:
|
||
|
|
- ForexFactory Calendar Scraping (kostenlos, keine API Key nötig)
|
||
|
|
- High-Impact Events Filter
|
||
|
|
- 30min Buffer vor/nach Event
|
||
|
|
- USD, EUR, GBP News (relevant für Gold)
|
||
|
|
- Cache für Performance
|
||
|
|
|
||
|
|
VERWENDUNG:
|
||
|
|
from news_filter import is_news_upcoming, get_upcoming_news
|
||
|
|
|
||
|
|
if is_news_upcoming(minutes_ahead=30):
|
||
|
|
print("⏸️ Trading SKIP: High-Impact News Event")
|
||
|
|
return
|
||
|
|
"""
|
||
|
|
|
||
|
|
import requests
|
||
|
|
from datetime import datetime, timedelta
|
||
|
|
from bs4 import BeautifulSoup
|
||
|
|
import json
|
||
|
|
import os
|
||
|
|
from typing import List, Dict, Optional
|
||
|
|
|
||
|
|
|
||
|
|
class EconomicCalendar:
|
||
|
|
"""
|
||
|
|
Economic Calendar - ForexFactory Scraper
|
||
|
|
"""
|
||
|
|
|
||
|
|
def __init__(self, cache_minutes: int = 15):
|
||
|
|
"""
|
||
|
|
Initialize Economic Calendar
|
||
|
|
|
||
|
|
Args:
|
||
|
|
cache_minutes: Cache duration in minutes
|
||
|
|
"""
|
||
|
|
self.cache_file = "news_cache.json"
|
||
|
|
self.cache_duration = timedelta(minutes=cache_minutes)
|
||
|
|
self.last_fetch = None
|
||
|
|
self.cached_events = []
|
||
|
|
|
||
|
|
|
||
|
|
def _get_forexfactory_calendar(self) -> List[Dict]:
|
||
|
|
"""
|
||
|
|
Scrape ForexFactory Calendar
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
List of news events
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
# ForexFactory calendar URL
|
||
|
|
url = "https://www.forexfactory.com/calendar"
|
||
|
|
|
||
|
|
headers = {
|
||
|
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
||
|
|
}
|
||
|
|
|
||
|
|
response = requests.get(url, headers=headers, timeout=10)
|
||
|
|
|
||
|
|
if response.status_code != 200:
|
||
|
|
print(f"⚠️ ForexFactory returned status {response.status_code}")
|
||
|
|
return []
|
||
|
|
|
||
|
|
soup = BeautifulSoup(response.content, 'html.parser')
|
||
|
|
|
||
|
|
# Parse calendar events
|
||
|
|
events = []
|
||
|
|
calendar_rows = soup.find_all('tr', class_='calendar__row')
|
||
|
|
|
||
|
|
current_date = datetime.now().date()
|
||
|
|
|
||
|
|
for row in calendar_rows:
|
||
|
|
try:
|
||
|
|
# Get time
|
||
|
|
time_elem = row.find('td', class_='calendar__time')
|
||
|
|
if not time_elem:
|
||
|
|
continue
|
||
|
|
|
||
|
|
time_text = time_elem.get_text(strip=True)
|
||
|
|
if not time_text or time_text in ['All Day', 'Day']:
|
||
|
|
continue
|
||
|
|
|
||
|
|
# Get currency
|
||
|
|
currency_elem = row.find('td', class_='calendar__currency')
|
||
|
|
if not currency_elem:
|
||
|
|
continue
|
||
|
|
currency = currency_elem.get_text(strip=True)
|
||
|
|
|
||
|
|
# Get impact
|
||
|
|
impact_elem = row.find('td', class_='calendar__impact')
|
||
|
|
if not impact_elem:
|
||
|
|
continue
|
||
|
|
|
||
|
|
impact_spans = impact_elem.find_all('span', class_='icon--ff-impact-gra')
|
||
|
|
impact_level = len(impact_spans) # 1=low, 2=medium, 3=high
|
||
|
|
|
||
|
|
# Get event name
|
||
|
|
event_elem = row.find('td', class_='calendar__event')
|
||
|
|
if not event_elem:
|
||
|
|
continue
|
||
|
|
event_name = event_elem.get_text(strip=True)
|
||
|
|
|
||
|
|
# Only track USD, EUR, GBP (relevant for Gold)
|
||
|
|
if currency not in ['USD', 'EUR', 'GBP']:
|
||
|
|
continue
|
||
|
|
|
||
|
|
# Only track high-impact events (level 3)
|
||
|
|
if impact_level < 3:
|
||
|
|
continue
|
||
|
|
|
||
|
|
# Parse time
|
||
|
|
try:
|
||
|
|
event_time = datetime.strptime(
|
||
|
|
f"{current_date} {time_text}",
|
||
|
|
"%Y-%m-%d %I:%M%p"
|
||
|
|
)
|
||
|
|
except:
|
||
|
|
continue
|
||
|
|
|
||
|
|
events.append({
|
||
|
|
'time': event_time.isoformat(),
|
||
|
|
'currency': currency,
|
||
|
|
'event': event_name,
|
||
|
|
'impact': 'HIGH',
|
||
|
|
'impact_level': impact_level
|
||
|
|
})
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
# Skip malformed rows
|
||
|
|
continue
|
||
|
|
|
||
|
|
return events
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
print(f"❌ Error fetching ForexFactory calendar: {e}")
|
||
|
|
return []
|
||
|
|
|
||
|
|
|
||
|
|
def _load_cache(self) -> bool:
|
||
|
|
"""Load events from cache"""
|
||
|
|
if not os.path.exists(self.cache_file):
|
||
|
|
return False
|
||
|
|
|
||
|
|
try:
|
||
|
|
with open(self.cache_file, 'r') as f:
|
||
|
|
data = json.load(f)
|
||
|
|
|
||
|
|
cache_time = datetime.fromisoformat(data['timestamp'])
|
||
|
|
|
||
|
|
# Check if cache is still valid
|
||
|
|
if datetime.now() - cache_time < self.cache_duration:
|
||
|
|
self.cached_events = data['events']
|
||
|
|
self.last_fetch = cache_time
|
||
|
|
return True
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
print(f"⚠️ Cache load error: {e}")
|
||
|
|
|
||
|
|
return False
|
||
|
|
|
||
|
|
|
||
|
|
def _save_cache(self, events: List[Dict]):
|
||
|
|
"""Save events to cache"""
|
||
|
|
try:
|
||
|
|
data = {
|
||
|
|
'timestamp': datetime.now().isoformat(),
|
||
|
|
'events': events
|
||
|
|
}
|
||
|
|
|
||
|
|
with open(self.cache_file, 'w') as f:
|
||
|
|
json.dump(data, f, indent=2)
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
print(f"⚠️ Cache save error: {e}")
|
||
|
|
|
||
|
|
|
||
|
|
def get_news_events(self, force_refresh: bool = False) -> List[Dict]:
|
||
|
|
"""
|
||
|
|
Get news events (cached or fresh)
|
||
|
|
|
||
|
|
Args:
|
||
|
|
force_refresh: Force fetch from API
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
List of news events
|
||
|
|
"""
|
||
|
|
# Try cache first
|
||
|
|
if not force_refresh and self._load_cache():
|
||
|
|
return self.cached_events
|
||
|
|
|
||
|
|
# Fetch fresh data
|
||
|
|
print("📰 Fetching economic calendar...")
|
||
|
|
events = self._get_forexfactory_calendar()
|
||
|
|
|
||
|
|
if events:
|
||
|
|
self.cached_events = events
|
||
|
|
self.last_fetch = datetime.now()
|
||
|
|
self._save_cache(events)
|
||
|
|
print(f"✅ Fetched {len(events)} high-impact events")
|
||
|
|
else:
|
||
|
|
print("⚠️ No events fetched, using cache if available")
|
||
|
|
self._load_cache()
|
||
|
|
|
||
|
|
return self.cached_events
|
||
|
|
|
||
|
|
|
||
|
|
def get_upcoming_news(self, minutes_ahead: int = 60, minutes_after: int = 30) -> List[Dict]:
|
||
|
|
"""
|
||
|
|
Get upcoming news events within time window
|
||
|
|
|
||
|
|
Args:
|
||
|
|
minutes_ahead: Look ahead this many minutes
|
||
|
|
minutes_after: Consider events from this many minutes ago
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
List of upcoming events
|
||
|
|
"""
|
||
|
|
events = self.get_news_events()
|
||
|
|
|
||
|
|
now = datetime.now()
|
||
|
|
start_window = now - timedelta(minutes=minutes_after)
|
||
|
|
end_window = now + timedelta(minutes=minutes_ahead)
|
||
|
|
|
||
|
|
upcoming = []
|
||
|
|
|
||
|
|
for event in events:
|
||
|
|
try:
|
||
|
|
event_time = datetime.fromisoformat(event['time'])
|
||
|
|
|
||
|
|
if start_window <= event_time <= end_window:
|
||
|
|
# Add time until event
|
||
|
|
delta = event_time - now
|
||
|
|
minutes_until = int(delta.total_seconds() / 60)
|
||
|
|
|
||
|
|
event_copy = event.copy()
|
||
|
|
event_copy['minutes_until'] = minutes_until
|
||
|
|
upcoming.append(event_copy)
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
continue
|
||
|
|
|
||
|
|
# Sort by time
|
||
|
|
upcoming.sort(key=lambda x: x['minutes_until'])
|
||
|
|
|
||
|
|
return upcoming
|
||
|
|
|
||
|
|
|
||
|
|
def is_news_upcoming(self, minutes_ahead: int = 30, minutes_after: int = 30) -> tuple:
|
||
|
|
"""
|
||
|
|
Check if high-impact news is upcoming
|
||
|
|
|
||
|
|
Args:
|
||
|
|
minutes_ahead: Block trading if news within this many minutes ahead
|
||
|
|
minutes_after: Block trading if news was this many minutes ago
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
(is_upcoming: bool, events: List[Dict])
|
||
|
|
"""
|
||
|
|
upcoming = self.get_upcoming_news(minutes_ahead, minutes_after)
|
||
|
|
|
||
|
|
return len(upcoming) > 0, upcoming
|
||
|
|
|
||
|
|
|
||
|
|
# ==========================================
|
||
|
|
# CONVENIENCE FUNCTIONS
|
||
|
|
# ==========================================
|
||
|
|
|
||
|
|
# Global calendar instance
|
||
|
|
_calendar = None
|
||
|
|
|
||
|
|
def get_calendar() -> EconomicCalendar:
|
||
|
|
"""Get or create global calendar instance"""
|
||
|
|
global _calendar
|
||
|
|
if _calendar is None:
|
||
|
|
_calendar = EconomicCalendar()
|
||
|
|
return _calendar
|
||
|
|
|
||
|
|
|
||
|
|
def is_news_upcoming(minutes_ahead: int = 30, minutes_after: int = 30) -> bool:
|
||
|
|
"""
|
||
|
|
Quick check if news is upcoming
|
||
|
|
|
||
|
|
Args:
|
||
|
|
minutes_ahead: Block if news within this many minutes
|
||
|
|
minutes_after: Block if news was this many minutes ago
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
True if news is upcoming, False otherwise
|
||
|
|
"""
|
||
|
|
calendar = get_calendar()
|
||
|
|
is_upcoming, events = calendar.is_news_upcoming(minutes_ahead, minutes_after)
|
||
|
|
|
||
|
|
if is_upcoming:
|
||
|
|
print("⏸️ HIGH-IMPACT NEWS DETECTED:")
|
||
|
|
for event in events:
|
||
|
|
minutes = event['minutes_until']
|
||
|
|
time_str = f"in {minutes}min" if minutes > 0 else f"{abs(minutes)}min ago"
|
||
|
|
print(f" • {event['currency']} {event['event']} - {time_str}")
|
||
|
|
|
||
|
|
return is_upcoming
|
||
|
|
|
||
|
|
|
||
|
|
def get_upcoming_news(minutes_ahead: int = 60) -> List[Dict]:
|
||
|
|
"""
|
||
|
|
Get list of upcoming news events
|
||
|
|
|
||
|
|
Args:
|
||
|
|
minutes_ahead: Look ahead this many minutes
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
List of news events
|
||
|
|
"""
|
||
|
|
calendar = get_calendar()
|
||
|
|
return calendar.get_upcoming_news(minutes_ahead)
|
||
|
|
|
||
|
|
|
||
|
|
def force_refresh_calendar():
|
||
|
|
"""Force refresh calendar data"""
|
||
|
|
calendar = get_calendar()
|
||
|
|
calendar.get_news_events(force_refresh=True)
|
||
|
|
|
||
|
|
|
||
|
|
# ==========================================
|
||
|
|
# TESTING
|
||
|
|
# ==========================================
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
print("="*70)
|
||
|
|
print("📰 NEWS FILTER - TEST")
|
||
|
|
print("="*70)
|
||
|
|
print()
|
||
|
|
|
||
|
|
# Test 1: Fetch calendar
|
||
|
|
print("Test 1: Fetching calendar...")
|
||
|
|
calendar = EconomicCalendar()
|
||
|
|
events = calendar.get_news_events(force_refresh=True)
|
||
|
|
print(f"✅ Found {len(events)} high-impact events")
|
||
|
|
print()
|
||
|
|
|
||
|
|
# Test 2: Show upcoming events
|
||
|
|
print("Test 2: Upcoming events (next 4 hours)...")
|
||
|
|
upcoming = calendar.get_upcoming_news(minutes_ahead=240)
|
||
|
|
|
||
|
|
if upcoming:
|
||
|
|
print(f"Found {len(upcoming)} upcoming events:")
|
||
|
|
for event in upcoming:
|
||
|
|
minutes = event['minutes_until']
|
||
|
|
time_str = f"in {minutes}min" if minutes > 0 else f"{abs(minutes)}min ago"
|
||
|
|
print(f" • {event['currency']} {event['event']} - {time_str}")
|
||
|
|
else:
|
||
|
|
print("No upcoming high-impact events in next 4 hours")
|
||
|
|
print()
|
||
|
|
|
||
|
|
# Test 3: Check if trading should be blocked
|
||
|
|
print("Test 3: Should trading be blocked? (30min window)")
|
||
|
|
is_upcoming, blocking_events = calendar.is_news_upcoming(
|
||
|
|
minutes_ahead=30,
|
||
|
|
minutes_after=30
|
||
|
|
)
|
||
|
|
|
||
|
|
if is_upcoming:
|
||
|
|
print("❌ YES - Trading should be BLOCKED")
|
||
|
|
print("Blocking events:")
|
||
|
|
for event in blocking_events:
|
||
|
|
minutes = event['minutes_until']
|
||
|
|
time_str = f"in {minutes}min" if minutes > 0 else f"{abs(minutes)}min ago"
|
||
|
|
print(f" • {event['currency']} {event['event']} - {time_str}")
|
||
|
|
else:
|
||
|
|
print("✅ NO - Trading is safe")
|
||
|
|
|
||
|
|
print()
|
||
|
|
print("="*70)
|
||
|
|
print("✅ TEST COMPLETE")
|
||
|
|
print("="*70)
|