news: DeepSeek validation, vs_forecast fix, /status source health
Validation (llm_news_eval.py harness + 78-headline golden CSV + verdict doc): headline scoring 78/78 vs keyword baseline 24/78 — validated; event gold-direction calls misread close K/M numeric comparisons ~11% of the time (both claims cases in sample, JOLTS 8.79M vs 8.84M). Fix: event payload now carries code-computed "vs_forecast" (from EconomicEvent.surprise) and the prompt gains an explicit inverted- metrics rule + worked claims example — iteration 1 (field alone) fixed the arithmetic but exposed a masked inversion error. 18/18 on live re-verification (4 misreads fixed, 11 regression cases clean, 3 fresh both-side probes). /status: new "News & AI" section — per-source health for the calendar chain (which of FF/MyFXBook served), each RSS feed, and DeepSeek (daily count, last error kind), written passively by the existing fetch paths under _cache_lock. Failures WARN once on transition (repeats DEBUG, recovery INFO) so the rotating log carries outage stories without 10-min spam. test_all.py: +5 Module 19 tests (351 total); also carries the Module 22 fixed-RR-TP tests from the prior research session (shared file). Replay artifacts dir gitignored like commit_ladder. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017mbXs9HRawZhD1uZ3ze7zQ
This commit is contained in:
@@ -71,6 +71,7 @@ from app_config import (
|
||||
telegram_channel_id,
|
||||
)
|
||||
from providers import BaseDataProvider, make_data_provider
|
||||
from llm_client import get_llm_client
|
||||
from news_engine import FundamentalContext, NewsEngine
|
||||
from pair_signals import PAIR_PRODUCERS
|
||||
from signal_aggregator import SignalAggregator, SignalReport
|
||||
@@ -1114,6 +1115,87 @@ async def cmd_help(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
await update.message.reply_text(_HELP_TEXT, parse_mode=ParseMode.MARKDOWN)
|
||||
|
||||
|
||||
def _fmt_age(ts: Optional[float]) -> str:
|
||||
"""Compact age like '45s', '12m', '3.2h' for status lines."""
|
||||
if not ts:
|
||||
return "?"
|
||||
s = time.time() - ts
|
||||
if s < 90:
|
||||
return f"{s:.0f}s"
|
||||
if s < 5400:
|
||||
return f"{s / 60:.0f}m"
|
||||
return f"{s / 3600:.1f}h"
|
||||
|
||||
|
||||
def _news_health_lines() -> List[str]:
|
||||
"""News / calendar / DeepSeek source health for /status.
|
||||
|
||||
Reads the class-level `NewsEngine._source_health` written by the
|
||||
scan-path fetches plus the LLM client's health stamps — display
|
||||
only, never triggers a fetch. ⚠️ marks a degraded dependency;
|
||||
details live in the rotating log (transition-logged WARNINGs)."""
|
||||
lines = ["\n*News & AI:*"]
|
||||
# Snapshot: a scan thread may insert a source's FIRST entry while we
|
||||
# iterate (dict-changed-size RuntimeError); entry mutation is safe.
|
||||
for _ in range(3):
|
||||
try:
|
||||
sh = dict(NewsEngine._source_health)
|
||||
break
|
||||
except RuntimeError:
|
||||
continue
|
||||
else:
|
||||
sh = {}
|
||||
if not sh:
|
||||
lines.append(" _no news cycle yet (fills after first scan)_")
|
||||
else:
|
||||
cal = sh.get("calendar")
|
||||
ff = sh.get("calendar:forexfactory")
|
||||
if cal:
|
||||
if NewsEngine.source_is_failing(cal):
|
||||
lines.append(f" ⚠️ Calendar: unavailable — {cal['detail']}"
|
||||
f" ({_fmt_age(cal['err'])})")
|
||||
elif NewsEngine.source_is_failing(ff):
|
||||
lines.append(f" ⚠️ Calendar: {cal['extra']} — ForexFactory"
|
||||
f" down: {ff['detail']} ({_fmt_age(ff['err'])})")
|
||||
else:
|
||||
lines.append(f" ✅ Calendar: {cal['extra']}"
|
||||
f" ({_fmt_age(cal['ok'])} ago)")
|
||||
heads = sh.get("headlines")
|
||||
if heads:
|
||||
failing = NewsEngine.source_is_failing(heads)
|
||||
mark = "⚠️" if failing else "✅"
|
||||
detail = f" — {heads['detail']}" if failing else ""
|
||||
ts = heads["err"] if failing else heads["ok"]
|
||||
lines.append(f" {mark} Headlines: {heads['extra']}{detail}"
|
||||
f" ({_fmt_age(ts)} ago)")
|
||||
for name, entry in sorted(sh.items()):
|
||||
if name.startswith("rss:") and NewsEngine.source_is_failing(entry):
|
||||
lines.append(f" ⚠️ {name[4:]}: {entry['detail']}"
|
||||
f" ({_fmt_age(entry['err'])})")
|
||||
client = get_llm_client()
|
||||
if not client.available:
|
||||
lines.append(" DeepSeek: off — keyword scoring only")
|
||||
else:
|
||||
# The client's counter only resets on the first call of a new
|
||||
# UTC day — show 0 (not yesterday's total, or a phantom
|
||||
# cap-exhausted warning) until that reset happens.
|
||||
today = datetime.now(timezone.utc).date()
|
||||
used = client._count if client._count_day == today else 0
|
||||
cap = client._daily_cap
|
||||
scored = len(NewsEngine._llm_sent_cache)
|
||||
err = client.last_error
|
||||
if used >= cap:
|
||||
lines.append(f" ⚠️ DeepSeek: daily cap {cap} exhausted —"
|
||||
" keyword fallback until UTC midnight")
|
||||
elif err and err[0] > client.last_ok:
|
||||
lines.append(f" ⚠️ DeepSeek: last call failed ({err[1]},"
|
||||
f" {_fmt_age(err[0])}) — keyword fallback")
|
||||
else:
|
||||
lines.append(f" ✅ DeepSeek: {used}/{cap} calls today,"
|
||||
f" {scored} headlines scored")
|
||||
return lines
|
||||
|
||||
|
||||
def _status_text() -> str:
|
||||
lines = ["📊 *ForexBot2 Status*\n"]
|
||||
|
||||
@@ -1172,6 +1254,8 @@ def _status_text() -> str:
|
||||
if active_breach:
|
||||
lines.append(f"*Breach tasks:* {active_breach} running")
|
||||
|
||||
lines.extend(_news_health_lines())
|
||||
|
||||
lines.append(f"\n*Alert quality:* {_alert_min_quality}+ "
|
||||
f"| re-arm {_ALERT_REARM_SECONDS // 60}m")
|
||||
return "\n".join(lines)
|
||||
|
||||
Reference in New Issue
Block a user