Fixed code

This commit is contained in:
2026-05-04 14:29:32 +00:00
parent 1637b93f1c
commit e4138ee60a
5 changed files with 908 additions and 889 deletions
+58 -117
View File
@@ -9,7 +9,6 @@ A perfect technical setup can be destroyed by an unexpected NFP print.
"""
import logging
import re
import xml.etree.ElementTree as ET
from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone
@@ -69,12 +68,13 @@ class FundamentalContext:
news_sentiment: Dict[str, float] = field(default_factory=dict) # currency -> sentiment
news_headlines: List[NewsSentiment] = field(default_factory=list)
event_impact_on_pairs: Dict[str, str] = field(default_factory=dict)
calendar_available: bool = False # True if live calendar data was fetched
class NewsEngine:
"""
Economic calendar and news sentiment analyzer.
Provides fundamental context that modifies technical signal confidence.
"""
@@ -126,7 +126,7 @@ class NewsEngine:
) -> FundamentalContext:
"""
Get complete fundamental context for signal decisions.
Combines economic calendar + news sentiment into actionable context.
"""
if currencies is None:
@@ -144,6 +144,7 @@ class NewsEngine:
e for e in events
if 0 < e.minutes_until <= 60
]
context.calendar_available = bool(events)
# Check for high-impact warnings
for event in events:
@@ -155,7 +156,7 @@ class NewsEngine:
)
break
# Map events to affected pairs
# Map upcoming high-impact events to affected pairs
for event in context.events_upcoming_1h:
if event.is_high_impact:
affected = self._get_affected_pairs(event.currency)
@@ -192,7 +193,6 @@ class NewsEngine:
Returns (risk_level, description).
risk_level: "clear", "caution", "danger"
"""
# Extract currencies from pair
clean = pair_symbol.replace("=X", "")
base = clean[:3]
quote = clean[3:]
@@ -237,11 +237,11 @@ class NewsEngine:
elif net > 0.3:
desc = f"Bullish sentiment ({base} positive / {quote} negative)"
elif net > 0:
desc = f"Slightly bullish sentiment"
desc = "Slightly bullish sentiment"
elif net < -0.3:
desc = f"Bearish sentiment ({base} negative / {quote} positive)"
else:
desc = f"Slightly bearish sentiment"
desc = "Slightly bearish sentiment"
return net, desc
@@ -249,104 +249,54 @@ class NewsEngine:
def _fetch_economic_calendar(self, currencies: List[str]) -> List[EconomicEvent]:
"""
Fetch economic calendar events.
Uses a lightweight approach since most free APIs require auth.
Falls back to manually constructed high-impact schedule.
Fetch economic calendar events from a free API.
Returns an empty list (with a log warning) if unavailable —
we never generate fake events since that causes constant false alarms.
"""
events = []
# Try fetching from a free calendar source
try:
events = self._try_fetch_calendar_api()
if events:
return [e for e in events if e.currency in currencies]
except Exception:
pass
except Exception as e:
logger.debug(f"Calendar API fetch exception: {e}")
# Fallback: generate known recurring high-impact events
events = self._generate_known_events(currencies)
return events
logger.info(
"Economic calendar data unavailable. "
"Event risk checks will be skipped for this cycle."
)
return []
def _try_fetch_calendar_api(self) -> List[EconomicEvent]:
"""Attempt to fetch from a free economic calendar API."""
# Using a simple RSS/JSON approach
"""Attempt to fetch from the Forex Factory free JSON calendar."""
events = []
try:
# Try Forex Factory RSS (most widely available)
url = "https://nfs.faireconomy.media/ff_calendar_thisweek.json"
resp = self._session.get(url, timeout=10)
if resp.status_code == 200:
data = resp.json()
for item in data:
try:
impact_map = {"High": "high", "Medium": "medium", "Low": "low"}
impact = impact_map.get(item.get("impact", ""), "low")
url = "https://nfs.faireconomy.media/ff_calendar_thisweek.json"
resp = self._session.get(url, timeout=10)
if resp.status_code != 200:
return []
# Parse date
date_str = item.get("date", "")
if date_str:
event_time = datetime.fromisoformat(
date_str.replace("Z", "+00:00")
)
else:
continue
data = resp.json()
impact_map = {"High": "high", "Medium": "medium", "Low": "low"}
events.append(EconomicEvent(
time=event_time,
currency=item.get("country", ""),
impact=impact,
event=item.get("title", ""),
forecast=item.get("forecast", None),
previous=item.get("previous", None),
actual=item.get("actual") if item.get("actual") else None,
))
except Exception:
continue
except Exception as e:
logger.debug(f"Calendar API unavailable: {e}")
for item in data:
try:
impact = impact_map.get(item.get("impact", ""), "low")
return events
date_str = item.get("date", "")
if not date_str:
continue
event_time = datetime.fromisoformat(date_str.replace("Z", "+00:00"))
def _generate_known_events(self, currencies: List[str]) -> List[EconomicEvent]:
"""
Generate a schedule of known recurring high-impact events.
This is the fallback when no API is available.
"""
events = []
now = datetime.now(timezone.utc)
today = now.date()
# Known high-impact regular events (approximate times)
recurring = [
("USD", "FOMC Rate Decision", 18, 0), # 2pm ET
("USD", "Non-Farm Payrolls", 12, 30), # 8:30am ET (first Friday)
("USD", "CPI Release", 12, 30),
("EUR", "ECB Rate Decision", 12, 45),
("GBP", "BOE Rate Decision", 11, 0),
("AUD", "RBA Rate Decision", 3, 30),
("CAD", "BOC Rate Decision", 14, 0),
("JPY", "BOJ Rate Decision", 3, 0),
("USD", "Initial Jobless Claims", 12, 30), # Weekly Thursday
("USD", "Retail Sales", 12, 30),
("EUR", "German ZEW", 9, 0),
]
# Only include if they'd be today or tomorrow
for currency, event_name, hour, minute in recurring:
if currency not in currencies:
continue
for day_offset in range(2):
event_date = today + timedelta(days=day_offset)
event_time = datetime(
event_date.year, event_date.month, event_date.day,
hour, minute, tzinfo=timezone.utc,
)
events.append(EconomicEvent(
time=event_time,
currency=currency,
impact="high",
event=event_name,
currency=item.get("country", ""),
impact=impact,
event=item.get("title", ""),
forecast=item.get("forecast") or None,
previous=item.get("previous") or None,
actual=item.get("actual") or None,
))
except Exception:
continue
return events
@@ -398,25 +348,17 @@ class NewsEngine:
def _analyze_headline_sentiment(self, headline: str) -> float:
"""
Simple but effective keyword-based sentiment analysis.
Simple keyword-based sentiment analysis.
Returns -1.0 (very bearish) to +1.0 (very bullish).
Note: This is intentionally simple. NLP models like FinBERT
would be better but require large dependencies. This catches
would be more accurate but require large dependencies. This catches
the most impactful sentiment signals from headlines.
"""
headline_lower = headline.lower()
bullish_score = 0
bearish_score = 0
for keyword in self.BULLISH_KEYWORDS:
if keyword in headline_lower:
bullish_score += 1
for keyword in self.BEARISH_KEYWORDS:
if keyword in headline_lower:
bearish_score += 1
bullish_score = sum(1 for kw in self.BULLISH_KEYWORDS if kw in headline_lower)
bearish_score = sum(1 for kw in self.BEARISH_KEYWORDS if kw in headline_lower)
total = bullish_score + bearish_score
if total == 0:
@@ -443,29 +385,28 @@ class NewsEngine:
def _extract_keywords(self, headline: str) -> List[str]:
"""Extract relevant trading keywords from headline."""
headline_lower = headline.lower()
found = []
for kw in self.BULLISH_KEYWORDS | self.BEARISH_KEYWORDS:
if kw in headline_lower:
found.append(kw)
return found
return [kw for kw in self.BULLISH_KEYWORDS | self.BEARISH_KEYWORDS if kw in headline_lower]
@staticmethod
def _parse_rss_date(date_str: Optional[str]) -> Optional[datetime]:
"""Parse various RSS date formats."""
"""Parse various RSS date formats into a timezone-aware datetime."""
if not date_str:
return None
formats = [
"%a, %d %b %Y %H:%M:%S %z",
"%a, %d %b %Y %H:%M:%S %Z",
"%Y-%m-%dT%H:%M:%S%z",
"%Y-%m-%d %H:%M:%S",
"%a, %d %b %Y %H:%M:%S %z", # RFC 822 with numeric tz offset
"%a, %d %b %Y %H:%M:%S %Z", # RFC 822 with tz abbreviation (GMT/UTC)
"%Y-%m-%dT%H:%M:%S%z", # ISO 8601 with offset
"%Y-%m-%d %H:%M:%S", # ISO 8601 naive (assume UTC)
]
for fmt in formats:
try:
return datetime.strptime(date_str.strip(), fmt).replace(
tzinfo=timezone.utc if fmt[-2:] != "%z" else None
)
dt = datetime.strptime(date_str.strip(), fmt)
# If strptime already attached tzinfo (from %z), keep it.
# Otherwise the string had no timezone info — assume UTC.
if dt.tzinfo is not None:
return dt
return dt.replace(tzinfo=timezone.utc)
except ValueError:
continue
return None
@@ -483,4 +424,4 @@ class NewsEngine:
"CAD": ["USDCAD=X", "CADJPY=X"],
"NZD": ["NZDUSD=X"],
}
return pairs.get(currency, [])
return pairs.get(currency, [])