Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95e026828b |
+1
-17
@@ -51,11 +51,7 @@ _DATE_RE = re.compile(
|
|||||||
r"|\bQ[1-4]\b",
|
r"|\bQ[1-4]\b",
|
||||||
flags=re.IGNORECASE,
|
flags=re.IGNORECASE,
|
||||||
)
|
)
|
||||||
# Hyphens/dashes are GNews query operators (a leading '-' means "exclude the
|
_PUNCT_RE = re.compile(r"[?!\"'.,;:()\[\]{}]")
|
||||||
# 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:
|
class NewsClient:
|
||||||
@@ -83,18 +79,6 @@ class NewsClient:
|
|||||||
# Public API
|
# 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:
|
async def get_sentiment(self, question: str) -> float:
|
||||||
"""
|
"""
|
||||||
Return a sentiment score ∈ [-1.0, +1.0] for the market question.
|
Return a sentiment score ∈ [-1.0, +1.0] for the market question.
|
||||||
|
|||||||
@@ -27,12 +27,6 @@ logging.basicConfig(
|
|||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
||||||
)
|
)
|
||||||
# httpx logs every request URL at INFO, and the GNews URL carries the API key as
|
|
||||||
# a `?token=` query param — that would leak GNEWS_API_KEY in plaintext into the
|
|
||||||
# pod logs. Raise httpx/httpcore to WARNING so request URLs never reach INFO.
|
|
||||||
# The bot's own GNews log lines only print the sanitised query, not the token.
|
|
||||||
logging.getLogger("httpx").setLevel(logging.WARNING)
|
|
||||||
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
|
||||||
log = logging.getLogger("bot.main")
|
log = logging.getLogger("bot.main")
|
||||||
|
|
||||||
PAPER_MODE = os.getenv("PAPER_MODE", "true").lower() == "true"
|
PAPER_MODE = os.getenv("PAPER_MODE", "true").lower() == "true"
|
||||||
|
|||||||
@@ -503,10 +503,7 @@ class BayesianStrategy:
|
|||||||
# Phase 3: caller has pre-sorted markets by gnews_priority() so the
|
# Phase 3: caller has pre-sorted markets by gnews_priority() so the
|
||||||
# highest-value markets reach this block first.
|
# highest-value markets reach this block first.
|
||||||
news_log_adj = 0.0
|
news_log_adj = 0.0
|
||||||
# self._news.enabled gates the whole block: with no GNews API key the
|
if is_politics and self._news is not None:
|
||||||
# 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:
|
if self._news_queries_this_cycle < MAX_NEWS_QUERIES_PER_CYCLE:
|
||||||
self._news_queries_this_cycle += 1
|
self._news_queries_this_cycle += 1
|
||||||
sentiment = await self._news.get_sentiment(market.question)
|
sentiment = await self._news.get_sentiment(market.question)
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
# Core
|
# Core
|
||||||
asyncpg==0.29.0
|
asyncpg==0.29.0
|
||||||
httpx==0.27.0
|
httpx==0.27.0
|
||||||
fastapi==0.111.0
|
fastapi==0.137.2
|
||||||
uvicorn[standard]==0.29.0
|
uvicorn[standard]==0.29.0
|
||||||
pydantic==2.7.0
|
pydantic==2.7.0
|
||||||
|
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
"""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
|
|
||||||
Reference in New Issue
Block a user