From 54fc8fa11af6cbc692b0733ac975935532f2cf91 Mon Sep 17 00:00:00 2001 From: chemavx Date: Fri, 26 Jun 2026 08:07:05 +0000 Subject: [PATCH] fix(news): strip GNews operator dashes and stop phantom query-budget counting Two minor faults found during the GNews capture/prioritisation diagnostic: 1. Hyphens/dashes reached the GNews query verbatim. '-' is GNews's exclusion operator, so a token like "El-Sayed" returned HTTP 400 and wasted a query. _PUNCT_RE now strips '-', en dash and em dash to spaces. 2. The per-cycle GNews budget counter incremented in evaluate() before get_sentiment() checked the API key, so with no key configured the [CYCLE SUMMARY] reported a phantom "gnews_queries_used: 5/5" with zero real requests. Added NewsClient.enabled and gated the GNews block on it; with no key the counter stays 0/5 and no spurious SKIP_GNEWS_PRIORITY is logged. No behaviour change when a key is present. Prioritisation itself was confirmed correct and is left untouched: politics markets are sorted by gnews_priority DESC and prior-extreme markets return before the budget is consumed, so no query is ever spent on a market that cannot trade. Tests: tests/test_news_query.py (4 new); full suite 66 passed. Co-Authored-By: Claude Opus 4.8 --- bot/data/news.py | 18 +++++++++- bot/strategy/bayesian.py | 5 ++- tests/test_news_query.py | 77 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 tests/test_news_query.py diff --git a/bot/data/news.py b/bot/data/news.py index 5c5b23e..23d8e18 100644 --- a/bot/data/news.py +++ b/bot/data/news.py @@ -51,7 +51,11 @@ _DATE_RE = re.compile( r"|\bQ[1-4]\b", flags=re.IGNORECASE, ) -_PUNCT_RE = re.compile(r"[?!\"'.,;:()\[\]{}]") +# Hyphens/dashes are GNews query operators (a leading '-' means "exclude the +# next term"), so a token like "El-Sayed" makes the API return HTTP 400. Strip +# them to spaces along with the rest of the punctuation so the query stays a +# plain keyword list. – = en dash, — = em dash. +_PUNCT_RE = re.compile(r"[?!\"'.,;:()\[\]{}\-–—]") class NewsClient: @@ -79,6 +83,18 @@ class NewsClient: # Public API # ------------------------------------------------------------------ + @property + def enabled(self) -> bool: + """True only when a GNews API key is configured. + + When False, get_sentiment() is a no-op that returns 0.0 without any + network call, so callers must skip GNews entirely — including the + per-cycle query budget accounting — instead of "spending" a query that + never reaches the API (which inflated gnews_queries_used to a phantom + 5/5 while the key was missing). + """ + return bool(self._api_key) + async def get_sentiment(self, question: str) -> float: """ Return a sentiment score ∈ [-1.0, +1.0] for the market question. diff --git a/bot/strategy/bayesian.py b/bot/strategy/bayesian.py index a20d08b..87518b8 100644 --- a/bot/strategy/bayesian.py +++ b/bot/strategy/bayesian.py @@ -503,7 +503,10 @@ class BayesianStrategy: # Phase 3: caller has pre-sorted markets by gnews_priority() so the # highest-value markets reach this block first. news_log_adj = 0.0 - if is_politics and self._news is not None: + # self._news.enabled gates the whole block: with no GNews API key the + # client is a no-op, so we must not consume (or report) query budget for + # it — see NewsClient.enabled. + if is_politics and self._news is not None and self._news.enabled: if self._news_queries_this_cycle < MAX_NEWS_QUERIES_PER_CYCLE: self._news_queries_this_cycle += 1 sentiment = await self._news.get_sentiment(market.question) diff --git a/tests/test_news_query.py b/tests/test_news_query.py new file mode 100644 index 0000000..6f46dc2 --- /dev/null +++ b/tests/test_news_query.py @@ -0,0 +1,77 @@ +"""Tests for the GNews layer minor fixes. + +Two faults found during the GNews capture/prioritisation diagnostic: + +1. Hyphens/dashes in a market question reached the GNews query verbatim and, + because '-' is GNews's exclusion operator, produced HTTP 400 + (e.g. "Abdul El-Sayed Michigan Democratic Primary"). + +2. The per-cycle GNews budget counter incremented in evaluate() *before* + get_sentiment() checked the API key, so with no key configured the + [CYCLE SUMMARY] reported a phantom "gnews_queries_used: 5/5" even though + zero real requests left the process. +""" +import asyncio + +from bot.data.news import NewsClient +from bot.data.external import ExternalSignals +from bot.data.polymarket import Market +from bot.strategy.bayesian import BayesianStrategy + + +# ── Fix 1: query sanitisation ──────────────────────────────────────────────── + +def test_build_query_strips_hyphen_that_breaks_gnews(): + q = NewsClient._build_query( + "Will Abdul El-Sayed win the 2026 Michigan Democratic Primary?" + ) + assert "-" not in q # the exclusion operator must be gone + assert "El-Sayed" not in q + assert "Sayed" in q # the meaningful token survives as its own word + + +def test_build_query_strips_unicode_dashes(): + q = NewsClient._build_query("Trump–Putin summit — final outcome") + assert "–" not in q and "—" not in q + assert "Trump" in q and "Putin" in q + + +# ── Fix 2: enabled property + budget accounting ────────────────────────────── + +def test_enabled_reflects_api_key(monkeypatch): + monkeypatch.delenv("GNEWS_API_KEY", raising=False) + assert NewsClient().enabled is False + monkeypatch.setenv("GNEWS_API_KEY", "deadbeefdeadbeefdeadbeefdeadbeef") + assert NewsClient().enabled is True + + +def _politics_market() -> Market: + return Market( + id="m1", condition_id="c1", + question="Will candidate X win the 2026 governor election?", + yes_token_id="y", no_token_id="n", + yes_price=0.50, no_price=0.50, volume_24h=10_000.0, + end_date="2026-07-15T00:00:00Z", active=True, category="politics", + ) + + +def _signals() -> ExternalSignals: + return ExternalSignals( + btc_price=1.0, btc_change_24h=0.0, eth_price=1.0, eth_change_24h=0.0, + btc_dominance=50.0, fear_greed_index=50, fear_greed_label="neutral", + total_market_cap_change=0.0, valid=True, + ) + + +def test_disabled_news_consumes_no_gnews_budget(monkeypatch): + """Regression: no API key → gnews_queries_used stays 0 (was a phantom 1+).""" + monkeypatch.delenv("GNEWS_API_KEY", raising=False) + news = NewsClient() + assert news.enabled is False + + strategy = BayesianStrategy(news=news, manifold=None, db=None) + strategy.reset_cycle() + asyncio.run( + strategy.evaluate(_politics_market(), _signals(), occupied_families=set()) + ) + assert strategy.get_cycle_stats()["gnews_queries_used"] == 0