feat: expand market coverage to politics, tech, and events categories
CI/CD / build-and-push (push) Successful in 1m31s

- 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 <noreply@anthropic.com>
This commit is contained in:
chemavx
2026-04-14 08:07:17 +00:00
parent f8c4f8b78a
commit b8d2b733fd
2 changed files with 86 additions and 13 deletions
+65 -9
View File
@@ -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
+21 -4
View File
@@ -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))