Fix news engine (keywords, calendar fallback, RSS URL), remove Telegram guards

News engine: expand sentiment keywords (18%→60% headline coverage), add
MyFXBook RSS as fallback calendar source when Forex Factory returns 429,
add retry/backoff on calendar API rate limits, update stale ForexLive URL
to investinglive.com, add date formats without seconds for MyFXBook RSS.

Telegram bot: remove flood control, rate limiter, and dashboard failure
tracking — all sends go directly through bot.send_message/edit_message_text.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
harwinder
2026-05-14 13:42:32 -04:00
parent bf9609708c
commit 57769967d1
3 changed files with 167 additions and 124 deletions
+145 -23
View File
@@ -9,6 +9,8 @@ A perfect technical setup can be destroyed by an unexpected NFP print.
"""
import logging
import re
import time
import xml.etree.ElementTree as ET
from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone
@@ -81,17 +83,26 @@ class NewsEngine:
# Keywords that indicate directional sentiment
BULLISH_KEYWORDS = {
"rate hike", "hawkish", "strong growth", "beat expectations",
"above forecast", "surplus", "expansion", "employment gains",
"above forecast", "above estimate", "above expectation",
"surplus", "expansion", "employment gains",
"strong jobs", "gdp growth", "inflation rising", "tightening",
"positive surprise", "record high", "bullish", "rally",
"stronger than expected", "upbeat",
"beats", "surges", "soars", "jumps", "climbs", "rises",
"higher than expected", "better than expected",
"strong data", "robust", "resilient", "accelerates",
}
BEARISH_KEYWORDS = {
"rate cut", "dovish", "recession", "slowdown", "contraction",
"below forecast", "deficit", "miss expectations", "weak data",
"below forecast", "below estimate", "below expectation",
"deficit", "miss expectations", "weak data",
"unemployment rise", "negative surprise", "bearish", "selloff",
"weaker than expected", "downbeat", "easing", "stimulus",
"misses", "slides", "drops", "falls", "plunges", "tumbles",
"weakens", "slumps", "declines", "retreats",
"lower than expected", "worse than expected",
"disappointing", "soft data",
}
# Currency-specific keywords for headline parsing
@@ -115,6 +126,8 @@ class NewsEngine:
"boe rate", "rba rate", "snb rate", "boc rate",
}
_calendar_backoff_until: float = 0.0
def __init__(self, config: NewsConfig):
self.config = config
self._session = requests.Session()
@@ -252,18 +265,27 @@ class NewsEngine:
# ── Private Methods ────────────────────────────────────────────
# Maps country names in MyFXBook titles to currency codes
_COUNTRY_TO_CURRENCY = {
"united states": "USD", "euro area": "EUR", "united kingdom": "GBP",
"japan": "JPY", "switzerland": "CHF", "australia": "AUD",
"canada": "CAD", "new zealand": "NZD",
}
def _fetch_economic_calendar(self, currencies: List[str]) -> List[EconomicEvent]:
"""
Fetch economic calendar events from a free API.
Tries Forex Factory first, falls back to MyFXBook RSS.
Returns an empty list (with a log warning) if unavailable —
we never generate fake events since that causes constant false alarms.
"""
try:
events = self._try_fetch_calendar_api()
if events:
return [e for e in events if e.currency in currencies]
except Exception as e:
logger.debug(f"Calendar API fetch exception: {e}")
for fetcher in (self._try_fetch_calendar_api, self._try_fetch_myfxbook_rss):
try:
events = fetcher()
if events:
return [e for e in events if e.currency in currencies]
except Exception as e:
logger.debug(f"Calendar fetch exception ({fetcher.__name__}): {e}")
logger.info(
"Economic calendar data unavailable. "
@@ -273,36 +295,134 @@ class NewsEngine:
def _try_fetch_calendar_api(self) -> List[EconomicEvent]:
"""Attempt to fetch from the Forex Factory free JSON calendar."""
events = []
now = time.time()
if now < NewsEngine._calendar_backoff_until:
wait = NewsEngine._calendar_backoff_until - now
logger.debug(f"Calendar API in backoff for {wait:.0f}s more")
return []
url = "https://nfs.faireconomy.media/ff_calendar_thisweek.json"
resp = self._session.get(url, timeout=10)
for attempt in range(3):
try:
resp = self._session.get(url, timeout=10)
except requests.RequestException as e:
logger.debug(f"Calendar request failed (attempt {attempt + 1}): {e}")
if attempt < 2:
time.sleep(2 ** attempt)
continue
if resp.status_code == 429:
retry_after = int(resp.headers.get("Retry-After", 300))
retry_after = min(retry_after, 600)
NewsEngine._calendar_backoff_until = time.time() + retry_after
logger.warning(
f"Calendar API rate-limited (429), backing off {retry_after}s"
)
return []
if resp.status_code != 200:
logger.debug(f"Calendar API returned {resp.status_code}")
return []
data = resp.json()
events = []
impact_map = {"High": "high", "Medium": "medium", "Low": "low"}
for item in data:
try:
impact = impact_map.get(item.get("impact", ""), "low")
date_str = item.get("date", "")
if not date_str:
continue
event_time = datetime.fromisoformat(date_str.replace("Z", "+00:00"))
events.append(EconomicEvent(
time=event_time,
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
return []
def _try_fetch_myfxbook_rss(self) -> List[EconomicEvent]:
"""Fallback calendar source: MyFXBook economic calendar RSS."""
url = "https://www.myfxbook.com/rss/forex-economic-calendar-events"
try:
resp = self._session.get(url, timeout=10)
except requests.RequestException as e:
logger.debug(f"MyFXBook calendar request failed: {e}")
return []
if resp.status_code != 200:
return []
data = resp.json()
impact_map = {"High": "high", "Medium": "medium", "Low": "low"}
try:
root = ET.fromstring(resp.content)
except ET.ParseError:
return []
for item in data:
events = []
for item in root.iter("item"):
try:
impact = impact_map.get(item.get("impact", ""), "low")
date_str = item.get("date", "")
if not date_str:
title_el = item.find("title")
pub_el = item.find("pubDate")
desc_el = item.find("description")
if title_el is None or title_el.text is None:
continue
event_time = datetime.fromisoformat(date_str.replace("Z", "+00:00"))
title = title_el.text.strip()
currency = ""
title_lower = title.lower()
for country, cur in self._COUNTRY_TO_CURRENCY.items():
if title_lower.startswith(country):
currency = cur
title = title[len(country):].strip()
break
if not currency:
continue
event_time = self._parse_rss_date(
pub_el.text if pub_el is not None else None
)
if event_time is None:
continue
impact = "low"
desc_text = desc_el.text if desc_el is not None and desc_el.text else ""
impact_match = re.search(r"sprite-(high|medium|low)-impact", desc_text)
if impact_match:
impact = impact_match.group(1)
cells = re.findall(r"<td[^>]*>(.*?)</td>", desc_text, re.DOTALL)
cells = [re.sub(r"<[^>]+>", "", c).strip() for c in cells]
previous = cells[2] if len(cells) > 2 and cells[2] else None
forecast = cells[3] if len(cells) > 3 and cells[3] else None
actual = cells[4] if len(cells) > 4 and cells[4] else None
events.append(EconomicEvent(
time=event_time,
currency=item.get("country", ""),
currency=currency,
impact=impact,
event=item.get("title", ""),
forecast=item.get("forecast") or None,
previous=item.get("previous") or None,
actual=item.get("actual") or None,
event=title,
forecast=forecast,
previous=previous,
actual=actual,
))
except Exception:
continue
logger.debug(f"MyFXBook calendar: parsed {len(events)} events")
return events
def _fetch_news_sentiment(self) -> List[NewsSentiment]:
@@ -401,6 +521,8 @@ class NewsEngine:
formats = [
"%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)
"%a, %d %b %Y %H:%M %Z", # RFC 822 without seconds (MyFXBook)
"%a, %d %b %Y %H:%M %z", # RFC 822 without seconds, numeric offset
"%Y-%m-%dT%H:%M:%S%z", # ISO 8601 with offset
"%Y-%m-%d %H:%M:%S", # ISO 8601 naive (assume UTC)
]