Fixed code
This commit is contained in:
+58
-117
@@ -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, [])
|
||||
|
||||
Reference in New Issue
Block a user