feat(news): 6h cache, politics-only, max 5/cycle, 2s sleep between calls
CI/CD / build-and-push (push) Successful in 1m32s

- CACHE_TTL: 4h → 6h (≤36 req/day with ≤9 politics markets)
- GNews only called for is_politics markets (BTC/F&G cover crypto/macro)
- MAX_NEWS_QUERIES_PER_CYCLE=5: BayesianStrategy.reset_cycle() called each
  iteration; counter increments only on actual API call (cache hits free)
- 2s asyncio.sleep in news.py finally block after each real HTTP request
- main.py sorts markets: politics first by end_date ascending, so soonest-
  resolving markets consume the 5-query budget before others

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
chemavx
2026-04-14 12:33:26 +00:00
parent d642dbd9cf
commit 33ad86f352
3 changed files with 54 additions and 11 deletions
+11 -4
View File
@@ -1,13 +1,16 @@
"""
News sentiment client for GNews API.
Free tier: 100 requests/day — we stay well within this by caching each
unique query for CACHE_TTL seconds (4 hours). With ~9 political markets
refreshed every 4 h that is 9 × 6 = 54 requests/day.
Free tier: 100 requests/day. Budget:
- Cache TTL: 6 hours — same query is never repeated within 6 h
- Max 5 queries per trading cycle (politics markets only)
- 2-second sleep between actual API calls to avoid burst 429s
With ≤9 politics markets and 6 h cache → ≤9 requests per 6 h = ≤36/day.
Score returned: -1.0 (very negative headlines) → +1.0 (very positive).
Returns 0.0 on any error or missing API key so the caller degrades gracefully.
"""
import asyncio
import logging
import os
import re
@@ -18,7 +21,8 @@ import httpx
log = logging.getLogger(__name__)
GNEWS_API = "https://gnews.io/api/v4/search"
CACHE_TTL = 4 * 3600 # seconds — fits 100 req/day free tier
CACHE_TTL = 6 * 3600 # seconds — ≤9 politics markets × 4 cycles/day = ≤36 req/day
_INTER_REQUEST_SLEEP = 2 # seconds between consecutive real API calls
# ---------------------------------------------------------------------------
# Keyword lists for headline sentiment
@@ -127,6 +131,9 @@ class NewsClient:
except Exception as exc:
log.warning("GNews network error for %r: %s", query, exc)
return 0.0
finally:
# Always sleep after a real network attempt to avoid burst 429s
await asyncio.sleep(_INTER_REQUEST_SLEEP)
log.info("GNews HTTP %d for query %r", resp.status_code, query)
+18 -1
View File
@@ -1,11 +1,12 @@
"""
Polymarket Trading Bot — Main Entry Point
# ci-test: 2026-04-13
# ci-test: 2026-04-14
"""
import asyncio
import logging
import os
from contextlib import asynccontextmanager
from datetime import datetime, timezone
from bot.data.polymarket import PolymarketClient
from bot.data.external import ExternalDataClient
@@ -42,6 +43,19 @@ async def run_trading_loop(
# 1. Fetch active crypto/finance markets
markets = await poly.get_active_markets()
log.info("Found %d active markets", len(markets))
# Sort: politics markets first (soonest-resolving → highest GNews priority),
# then all others. This ensures the 5-query-per-cycle cap hits the most
# time-sensitive political markets before the budget runs out.
def _sort_key(m):
is_pol = m.category == "politics"
try:
dt = datetime.fromisoformat(m.end_date.replace("Z", "+00:00"))
except Exception:
dt = datetime(9999, 12, 31, tzinfo=timezone.utc)
return (0 if is_pol else 1, dt)
markets = sorted(markets, key=_sort_key)
for _m in markets:
log.info(" [market] %s | ends: %s | yes_price: %.3f",
_m.question, _m.end_date, _m.yes_price)
@@ -49,6 +63,9 @@ async def run_trading_loop(
# 2. Get external signals
ext_data = await external.get_all_signals()
# Reset per-cycle GNews counter so the limit applies fresh each cycle
strategy.reset_cycle()
for market in markets:
# 3. Estimate true probability
signal = await strategy.evaluate(market, ext_data)
+21 -2
View File
@@ -32,6 +32,10 @@ MIN_CONFIDENCE = 0.55 # Minimum confidence in our estimate
# which moves a 50% prior to ~18%/82% — strong but not overwhelming.
NEWS_LOGODDS_WEIGHT = 1.5
# GNews free tier: 100 req/day. We limit to 5 queries per trading cycle
# (politics markets only) and rely on 6 h cache to stay within budget.
MAX_NEWS_QUERIES_PER_CYCLE = 5
@dataclass
class TradingSignal:
@@ -62,6 +66,11 @@ class BayesianStrategy:
def __init__(self, news: Optional[NewsClient] = None) -> None:
self._signal_count = 0
self._news = news # Optional; degrades gracefully when None or key missing
self._news_queries_this_cycle = 0
def reset_cycle(self) -> None:
"""Call once at the start of each trading cycle to reset the per-cycle GNews counter."""
self._news_queries_this_cycle = 0
async def evaluate(
self,
@@ -172,16 +181,26 @@ class BayesianStrategy:
adjustments.append(dom_adj)
sources.append(f"BTC dom: {ext.btc_dominance:.1f}% (low → alt season)")
# Signal 4: GNews sentiment (politics / tech / events only)
# Signal 4: GNews sentiment politics markets only.
# BTC/F&G already cover crypto and macro; GNews budget is too tight to
# waste on tech/events. Cap at MAX_NEWS_QUERIES_PER_CYCLE per cycle so
# we prioritise the soonest-resolving markets (caller sorts by end_date).
# Applied as a direct log-odds shift — stronger signal than macro proxies.
# Weight NEWS_LOGODDS_WEIGHT=1.5 means a ±1.0 sentiment score shifts
# log-odds by ±1.5 (e.g. 50% prior → ~82% / ~18%).
news_log_adj = 0.0
if (is_politics or is_tech or is_events) and self._news is not None:
if is_politics and self._news is not None:
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)
if abs(sentiment) > 0.05:
news_log_adj = sentiment * NEWS_LOGODDS_WEIGHT
sources.append(f"GNews: {sentiment:+.2f}")
else:
log.debug(
"GNews cycle limit (%d) reached — skipping news for %r",
MAX_NEWS_QUERIES_PER_CYCLE, market.question[:50],
)
# Macro/politics/tech/events: cap confidence lower to reflect weaker signal quality
if is_macro or is_politics or is_tech or is_events: