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.
This commit is contained in:
+21
-100
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user