From b8d2b733fd06b3ab7e98f2910e62553ae29d821f Mon Sep 17 00:00:00 2001 From: chemavx Date: Tue, 14 Apr 2026 08:07:17 +0000 Subject: [PATCH] feat: expand market coverage to politics, tech, and events categories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - polymarket.py: add keyword lists for politics (election, trump, ukraine…), tech (AI, OpenAI, Apple, nvidia…), and events (super bowl, oscar, spacex…); introduce _detect_category() so all four categories flow through a single code path; filter already-expired markets (end_dt < now) in addition to the existing future-cutoff filter; log per-category counts at startup - bayesian.py: extend is_any_supported to include is_politics / is_tech / is_events; use BTC as a risk-sentiment proxy for non-crypto categories (halved weight to reflect weaker correlation); cap confidence_cap at 0.65 for macro/politics/tech/events; MIN_EDGE stays at 0.10 Co-Authored-By: Claude Sonnet 4.6 --- bot/data/polymarket.py | 74 +++++++++++++++++++++++++++++++++++----- bot/strategy/bayesian.py | 25 +++++++++++--- 2 files changed, 86 insertions(+), 13 deletions(-) diff --git a/bot/data/polymarket.py b/bot/data/polymarket.py index 6fc5bc2..a74809a 100644 --- a/bot/data/polymarket.py +++ b/bot/data/polymarket.py @@ -69,11 +69,53 @@ class PolymarketClient: " ipo ", "sec ", "cftc", ] + _POLITICS_KEYWORDS: list[str] = [ + "election", "president", "congress", "senate", "vote", "war", + "trump", "biden", "ukraine", "russia", "israel", "nato", + ] + + _TECH_KEYWORDS: list[str] = [ + " ai ", "openai", "apple", "google", "microsoft", "meta", + "nvidia", "regulation", "antitrust", + ] + + _EVENTS_KEYWORDS: list[str] = [ + "super bowl", "world cup", "oscar", "nobel", "spacex", "nasa", + ] + @classmethod def _is_crypto_finance(cls, question: str) -> bool: q = f" {question.lower()} " # pad so edge keywords match cleanly return any(kw in q for kw in cls._CRYPTO_FINANCE_KEYWORDS) + @classmethod + def _is_politics(cls, question: str) -> bool: + q = f" {question.lower()} " + return any(kw in q for kw in cls._POLITICS_KEYWORDS) + + @classmethod + def _is_tech(cls, question: str) -> bool: + q = f" {question.lower()} " + return any(kw in q for kw in cls._TECH_KEYWORDS) + + @classmethod + def _is_events(cls, question: str) -> bool: + q = f" {question.lower()} " + return any(kw in q for kw in cls._EVENTS_KEYWORDS) + + @classmethod + def _detect_category(cls, question: str) -> str: + """Return the category label for a market question, or '' if unsupported.""" + if cls._is_crypto_finance(question): + return "crypto/finance" + if cls._is_politics(question): + return "politics" + if cls._is_tech(question): + return "tech" + if cls._is_events(question): + return "events" + return "" + async def get_active_markets( self, min_volume: float = 1000, @@ -81,15 +123,18 @@ class PolymarketClient: page_size: int = 200, max_days_to_resolution: int = 30, ) -> list[Market]: - """Fetch active crypto/finance markets from Gamma API (no auth needed). + """Fetch active markets from Gamma API (no auth needed). Fetches events without tag filtering (tag= param is unreliable), - then keeps only markets whose question matches crypto/finance keywords - and that resolve within max_days_to_resolution days. + then keeps only markets whose question matches any supported category + (crypto/finance, politics, tech, events) and that: + - have NOT already expired (end_dt >= now) + - resolve within max_days_to_resolution days """ seen: set[str] = set() markets: list[Market] = [] - cutoff = datetime.now(timezone.utc) + timedelta(days=max_days_to_resolution) + now = datetime.now(timezone.utc) + cutoff = now + timedelta(days=max_days_to_resolution) for page in range(pages): try: @@ -116,11 +161,15 @@ class PolymarketClient: continue question = m.get("question", "") - if not self._is_crypto_finance(question) and \ - not self._is_crypto_finance(event_title): + + # Detect category from question or event title + category = self._detect_category(question) + if not category: + category = self._detect_category(event_title) + if not category: continue - # Filter: only markets resolving within the cutoff window + # Filter: skip already-expired and far-future markets # Gamma API may return endDate or end_date (snake_case) raw_end = m.get("endDate") or m.get("end_date") or m.get("endDateIso", "") if raw_end: @@ -131,6 +180,9 @@ class PolymarketClient: # Make naive datetimes UTC-aware before comparing if end_dt.tzinfo is None: end_dt = end_dt.replace(tzinfo=timezone.utc) + if end_dt < now: + log.debug("Skipping expired market: %s", question[:60]) + continue if end_dt > cutoff: continue except (ValueError, TypeError): @@ -167,7 +219,7 @@ class PolymarketClient: volume_24h=vol, end_date=m.get("endDate", ""), active=True, - category="crypto/finance", + category=category, )) except (KeyError, ValueError, IndexError) as e: log.debug("Skipping malformed market: %s", e) @@ -176,9 +228,13 @@ class PolymarketClient: log.error("Polymarket API error (page=%d): %s", page, e) break + by_cat: dict[str, int] = {} + for mkt in markets: + by_cat[mkt.category] = by_cat.get(mkt.category, 0) + 1 log.info( - "Loaded %d crypto/finance markets (min_vol=%.0f, resolving within %dd)", + "Loaded %d markets (min_vol=%.0f, resolving within %dd): %s", len(markets), min_volume, max_days_to_resolution, + ", ".join(f"{k}={v}" for k, v in sorted(by_cat.items())), ) return markets diff --git a/bot/strategy/bayesian.py b/bot/strategy/bayesian.py index 358090a..69a81a6 100644 --- a/bot/strategy/bayesian.py +++ b/bot/strategy/bayesian.py @@ -66,6 +66,7 @@ class BayesianStrategy: Returns None if no actionable opportunity. """ question_lower = market.question.lower() + category = market.category # set by PolymarketClient # Classify what kind of market this is is_price_above = any(w in question_lower for w in ["above", "over", "exceed", "higher", "atleast", "reach"]) @@ -85,8 +86,14 @@ class BayesianStrategy: is_macro = any( w in question_lower for w in ["nasdaq", "s&p", "sp500", "inflation", "fed rate", "interest rate", "tariff"] ) + is_politics = category == "politics" + is_tech = category == "tech" + is_events = category == "events" - is_any_supported = is_btc or is_eth or is_altcoin or is_general_crypto or is_macro + is_any_supported = ( + is_btc or is_eth or is_altcoin or is_general_crypto or is_macro + or is_politics or is_tech or is_events + ) if not is_any_supported: log.debug("Skipping unsupported market: %s", market.question[:60]) return None @@ -103,12 +110,17 @@ class BayesianStrategy: adjustments: list[float] = [] # Signal 1: Price momentum (asset-specific or total market cap as proxy) + # For politics/tech/events use BTC as a broad sentiment proxy. if is_btc: momentum = ext.btc_change_24h asset_label = "BTC" elif is_eth: momentum = ext.eth_change_24h asset_label = "ETH" + elif is_politics or is_tech or is_events: + # BTC as risk-sentiment proxy for non-crypto categories + momentum = ext.btc_change_24h + asset_label = "BTC(sentiment)" else: # Altcoins and general crypto: use total market cap change as proxy momentum = ext.total_market_cap_change @@ -116,6 +128,9 @@ class BayesianStrategy: if abs(momentum) > 2: momentum_adj = math.tanh(momentum / 20) * 0.15 # Max ±15% + # For non-directional markets (politics/events/tech), momentum is weaker signal + if is_politics or is_tech or is_events: + momentum_adj *= 0.5 adjustments.append(momentum_adj if is_price_above else -momentum_adj) sources.append(f"{asset_label} 24h: {momentum:+.1f}%") @@ -143,9 +158,11 @@ class BayesianStrategy: adjustments.append(dom_adj) sources.append(f"BTC dom: {ext.btc_dominance:.1f}% (low → alt season)") - # Macro markets: lower weight, rely only on Fear&Greed signal already added - # Cap confidence below for macro to reflect weaker signal quality - confidence_cap = 0.70 if is_macro else 0.90 + # Macro/politics/tech/events: cap confidence lower to reflect weaker signal quality + if is_macro or is_politics or is_tech or is_events: + confidence_cap = 0.65 + else: + confidence_cap = 0.90 # Compute posterior using log-odds updating log_odds_prior = math.log(prior / (1 - prior))