feat(news): 6h cache, politics-only, max 5/cycle, 2s sleep between calls
CI/CD / build-and-push (push) Successful in 1m32s
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:
+11
-4
@@ -1,13 +1,16 @@
|
|||||||
"""
|
"""
|
||||||
News sentiment client for GNews API.
|
News sentiment client for GNews API.
|
||||||
|
|
||||||
Free tier: 100 requests/day — we stay well within this by caching each
|
Free tier: 100 requests/day. Budget:
|
||||||
unique query for CACHE_TTL seconds (4 hours). With ~9 political markets
|
- Cache TTL: 6 hours — same query is never repeated within 6 h
|
||||||
refreshed every 4 h that is 9 × 6 = 54 requests/day.
|
- 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).
|
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.
|
Returns 0.0 on any error or missing API key so the caller degrades gracefully.
|
||||||
"""
|
"""
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
@@ -18,7 +21,8 @@ import httpx
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
GNEWS_API = "https://gnews.io/api/v4/search"
|
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
|
# Keyword lists for headline sentiment
|
||||||
@@ -127,6 +131,9 @@ class NewsClient:
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
log.warning("GNews network error for %r: %s", query, exc)
|
log.warning("GNews network error for %r: %s", query, exc)
|
||||||
return 0.0
|
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)
|
log.info("GNews HTTP %d for query %r", resp.status_code, query)
|
||||||
|
|
||||||
|
|||||||
+18
-1
@@ -1,11 +1,12 @@
|
|||||||
"""
|
"""
|
||||||
Polymarket Trading Bot — Main Entry Point
|
Polymarket Trading Bot — Main Entry Point
|
||||||
# ci-test: 2026-04-13
|
# ci-test: 2026-04-14
|
||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from bot.data.polymarket import PolymarketClient
|
from bot.data.polymarket import PolymarketClient
|
||||||
from bot.data.external import ExternalDataClient
|
from bot.data.external import ExternalDataClient
|
||||||
@@ -42,6 +43,19 @@ async def run_trading_loop(
|
|||||||
# 1. Fetch active crypto/finance markets
|
# 1. Fetch active crypto/finance markets
|
||||||
markets = await poly.get_active_markets()
|
markets = await poly.get_active_markets()
|
||||||
log.info("Found %d active markets", len(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:
|
for _m in markets:
|
||||||
log.info(" [market] %s | ends: %s | yes_price: %.3f",
|
log.info(" [market] %s | ends: %s | yes_price: %.3f",
|
||||||
_m.question, _m.end_date, _m.yes_price)
|
_m.question, _m.end_date, _m.yes_price)
|
||||||
@@ -49,6 +63,9 @@ async def run_trading_loop(
|
|||||||
# 2. Get external signals
|
# 2. Get external signals
|
||||||
ext_data = await external.get_all_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:
|
for market in markets:
|
||||||
# 3. Estimate true probability
|
# 3. Estimate true probability
|
||||||
signal = await strategy.evaluate(market, ext_data)
|
signal = await strategy.evaluate(market, ext_data)
|
||||||
|
|||||||
@@ -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.
|
# which moves a 50% prior to ~18%/82% — strong but not overwhelming.
|
||||||
NEWS_LOGODDS_WEIGHT = 1.5
|
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
|
@dataclass
|
||||||
class TradingSignal:
|
class TradingSignal:
|
||||||
@@ -62,6 +66,11 @@ class BayesianStrategy:
|
|||||||
def __init__(self, news: Optional[NewsClient] = None) -> None:
|
def __init__(self, news: Optional[NewsClient] = None) -> None:
|
||||||
self._signal_count = 0
|
self._signal_count = 0
|
||||||
self._news = news # Optional; degrades gracefully when None or key missing
|
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(
|
async def evaluate(
|
||||||
self,
|
self,
|
||||||
@@ -172,16 +181,26 @@ class BayesianStrategy:
|
|||||||
adjustments.append(dom_adj)
|
adjustments.append(dom_adj)
|
||||||
sources.append(f"BTC dom: {ext.btc_dominance:.1f}% (low → alt season)")
|
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.
|
# 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
|
# Weight NEWS_LOGODDS_WEIGHT=1.5 means a ±1.0 sentiment score shifts
|
||||||
# log-odds by ±1.5 (e.g. 50% prior → ~82% / ~18%).
|
# log-odds by ±1.5 (e.g. 50% prior → ~82% / ~18%).
|
||||||
news_log_adj = 0.0
|
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)
|
sentiment = await self._news.get_sentiment(market.question)
|
||||||
if abs(sentiment) > 0.05:
|
if abs(sentiment) > 0.05:
|
||||||
news_log_adj = sentiment * NEWS_LOGODDS_WEIGHT
|
news_log_adj = sentiment * NEWS_LOGODDS_WEIGHT
|
||||||
sources.append(f"GNews: {sentiment:+.2f}")
|
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
|
# Macro/politics/tech/events: cap confidence lower to reflect weaker signal quality
|
||||||
if is_macro or is_politics or is_tech or is_events:
|
if is_macro or is_politics or is_tech or is_events:
|
||||||
|
|||||||
Reference in New Issue
Block a user