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
+21 -100
View File
@@ -131,77 +131,6 @@ AUTOSCAN_CONFIRMATIONS = {
_executor = ThreadPoolExecutor(max_workers=4)
# ── Telegram rate limiter with flood control ─────────────────────────
_send_lock = asyncio.Lock()
_last_send_per_chat: Dict[int, float] = {}
_TELEGRAM_MIN_INTERVAL = 1.5
_TELEGRAM_SEND_TIMEOUT = 30
_flood_until: float = 0.0
def _is_flood_blocked() -> bool:
return time.time() < _flood_until
def _handle_retry_after(retry_seconds: int) -> None:
global _flood_until
_flood_until = time.time() + retry_seconds
logger.warning(
f"Telegram flood control: pausing ALL sends for {retry_seconds}s "
f"(until {datetime.fromtimestamp(_flood_until, tz=timezone.utc).strftime('%H:%M:%S UTC')})"
)
async def _rate_limited_send(bot, chat_id: int, text: str, **kwargs) -> Optional[object]:
if _is_flood_blocked():
logger.debug(f"Send skipped — flood backoff active ({_flood_until - time.time():.0f}s left)")
return None
async with _send_lock:
now = time.time()
last = _last_send_per_chat.get(chat_id, 0)
wait = _TELEGRAM_MIN_INTERVAL - (now - last)
if wait > 0:
await asyncio.sleep(wait)
try:
result = await asyncio.wait_for(
bot.send_message(chat_id, text, **kwargs),
timeout=_TELEGRAM_SEND_TIMEOUT,
)
_last_send_per_chat[chat_id] = time.time()
return result
except asyncio.TimeoutError:
logger.error(f"Telegram send_message timed out for chat {chat_id}")
return None
except Exception as exc:
if "retry" in str(exc).lower() and hasattr(exc, "retry_after"):
_handle_retry_after(int(exc.retry_after))
else:
logger.exception(f"Telegram send_message failed for chat {chat_id}")
return None
async def _rate_limited_edit(bot, text: str, chat_id: int, message_id: int, **kwargs) -> bool:
if _is_flood_blocked():
return False
try:
await asyncio.wait_for(
bot.edit_message_text(text, chat_id=chat_id, message_id=message_id, **kwargs),
timeout=_TELEGRAM_SEND_TIMEOUT,
)
return True
except asyncio.TimeoutError:
logger.error(f"Telegram edit_message timed out for chat {chat_id}")
return False
except Exception as exc:
if "retry" in str(exc).lower() and hasattr(exc, "retry_after"):
_handle_retry_after(int(exc.retry_after))
return False
if "message is not modified" not in str(exc).lower():
logger.warning(f"Telegram edit_message failed for chat {chat_id}: {exc}")
return False
# ── Markdown escaping ─────────────────────────────────────────────────
def _escape_md(text: str) -> str:
@@ -470,30 +399,24 @@ def _format_stats_dashboard() -> str:
_dashboard_msg_ids: Dict[int, int] = {}
_dashboard_task: Optional[asyncio.Task] = None
_last_dashboard_refresh: float = 0
_DASHBOARD_THROTTLE = 30
_DASHBOARD_LOOP_INTERVAL = 30
_DASHBOARD_REFRESH_INTERVAL = 30
_dashboard_lock = asyncio.Lock()
_dashboard_edit_failures: Dict[int, int] = {}
_DASHBOARD_MAX_EDIT_FAILURES = 3
async def _send_dashboard(bot, chat_id: int) -> None:
if _is_flood_blocked():
return
text = _format_stats_dashboard()
msg = await _rate_limited_send(bot, chat_id, text, parse_mode=ParseMode.MARKDOWN)
if msg:
try:
msg = await bot.send_message(chat_id, text, parse_mode=ParseMode.MARKDOWN)
_dashboard_msg_ids[chat_id] = msg.message_id
_dashboard_edit_failures[chat_id] = 0
except Exception as exc:
logger.warning(f"Dashboard send failed for chat {chat_id}: {exc}")
async def _refresh_dashboard(bot, force: bool = False) -> None:
global _last_dashboard_refresh
if _is_flood_blocked():
return
async with _dashboard_lock:
now = time.time()
if not force and (now - _last_dashboard_refresh) < _DASHBOARD_THROTTLE:
if not force and (now - _last_dashboard_refresh) < _DASHBOARD_REFRESH_INTERVAL:
return
_last_dashboard_refresh = now
@@ -503,25 +426,20 @@ async def _refresh_dashboard(bot, force: bool = False) -> None:
if not msg_id:
await _send_dashboard(bot, chat_id)
continue
ok = await _rate_limited_edit(
bot, text, chat_id=chat_id, message_id=msg_id,
parse_mode=ParseMode.MARKDOWN,
)
if not ok:
failures = _dashboard_edit_failures.get(chat_id, 0) + 1
_dashboard_edit_failures[chat_id] = failures
if failures >= _DASHBOARD_MAX_EDIT_FAILURES:
logger.info(f"Dashboard edit failed {failures}x for chat {chat_id}, sending new message")
_dashboard_edit_failures[chat_id] = 0
await _send_dashboard(bot, chat_id)
else:
_dashboard_edit_failures[chat_id] = 0
try:
await bot.edit_message_text(
text, chat_id=chat_id, message_id=msg_id,
parse_mode=ParseMode.MARKDOWN,
)
except Exception as exc:
if "message is not modified" not in str(exc).lower():
logger.warning(f"Dashboard edit failed for chat {chat_id}: {exc}")
async def _dashboard_loop(bot) -> None:
try:
while _autoscan_chats:
await asyncio.sleep(_DASHBOARD_LOOP_INTERVAL)
await asyncio.sleep(_DASHBOARD_REFRESH_INTERVAL)
if not _autoscan_chats:
break
await _refresh_dashboard(bot)
@@ -623,7 +541,10 @@ async def _scan_timeframe(
for chat_id in list(_autoscan_chats):
for report in actionable:
detail = format_signal_message(report)
await _rate_limited_send(bot, chat_id, detail, parse_mode=ParseMode.MARKDOWN)
try:
await bot.send_message(chat_id, detail, parse_mode=ParseMode.MARKDOWN)
except Exception as exc:
logger.warning(f"Failed to send signal for chat {chat_id}: {exc}")
# ── WebSocket level-breach monitor ──────────────────────────────────
@@ -1065,8 +986,8 @@ async def _post_init(app: Application) -> None:
)
try:
await _rate_limited_send(
app.bot, chat_id,
await app.bot.send_message(
chat_id,
f"🤖 *ForexBot2 started*\n\n"
f"Auto-scanning {len(SCAN_PAIRS)} pairs on M5/M15/H1/H4.\n"
f"📊 Live dashboard refreshes every 60s.\n"