feat: expand market coverage to politics, tech, and events categories
CI/CD / build-and-push (push) Successful in 1m31s
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:
+65
-9
@@ -69,11 +69,53 @@ class PolymarketClient:
|
|||||||
" ipo ", "sec ", "cftc",
|
" 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
|
@classmethod
|
||||||
def _is_crypto_finance(cls, question: str) -> bool:
|
def _is_crypto_finance(cls, question: str) -> bool:
|
||||||
q = f" {question.lower()} " # pad so edge keywords match cleanly
|
q = f" {question.lower()} " # pad so edge keywords match cleanly
|
||||||
return any(kw in q for kw in cls._CRYPTO_FINANCE_KEYWORDS)
|
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(
|
async def get_active_markets(
|
||||||
self,
|
self,
|
||||||
min_volume: float = 1000,
|
min_volume: float = 1000,
|
||||||
@@ -81,15 +123,18 @@ class PolymarketClient:
|
|||||||
page_size: int = 200,
|
page_size: int = 200,
|
||||||
max_days_to_resolution: int = 30,
|
max_days_to_resolution: int = 30,
|
||||||
) -> list[Market]:
|
) -> 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),
|
Fetches events without tag filtering (tag= param is unreliable),
|
||||||
then keeps only markets whose question matches crypto/finance keywords
|
then keeps only markets whose question matches any supported category
|
||||||
and that resolve within max_days_to_resolution days.
|
(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()
|
seen: set[str] = set()
|
||||||
markets: list[Market] = []
|
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):
|
for page in range(pages):
|
||||||
try:
|
try:
|
||||||
@@ -116,11 +161,15 @@ class PolymarketClient:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
question = m.get("question", "")
|
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
|
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)
|
# Gamma API may return endDate or end_date (snake_case)
|
||||||
raw_end = m.get("endDate") or m.get("end_date") or m.get("endDateIso", "")
|
raw_end = m.get("endDate") or m.get("end_date") or m.get("endDateIso", "")
|
||||||
if raw_end:
|
if raw_end:
|
||||||
@@ -131,6 +180,9 @@ class PolymarketClient:
|
|||||||
# Make naive datetimes UTC-aware before comparing
|
# Make naive datetimes UTC-aware before comparing
|
||||||
if end_dt.tzinfo is None:
|
if end_dt.tzinfo is None:
|
||||||
end_dt = end_dt.replace(tzinfo=timezone.utc)
|
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:
|
if end_dt > cutoff:
|
||||||
continue
|
continue
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
@@ -167,7 +219,7 @@ class PolymarketClient:
|
|||||||
volume_24h=vol,
|
volume_24h=vol,
|
||||||
end_date=m.get("endDate", ""),
|
end_date=m.get("endDate", ""),
|
||||||
active=True,
|
active=True,
|
||||||
category="crypto/finance",
|
category=category,
|
||||||
))
|
))
|
||||||
except (KeyError, ValueError, IndexError) as e:
|
except (KeyError, ValueError, IndexError) as e:
|
||||||
log.debug("Skipping malformed market: %s", e)
|
log.debug("Skipping malformed market: %s", e)
|
||||||
@@ -176,9 +228,13 @@ class PolymarketClient:
|
|||||||
log.error("Polymarket API error (page=%d): %s", page, e)
|
log.error("Polymarket API error (page=%d): %s", page, e)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
by_cat: dict[str, int] = {}
|
||||||
|
for mkt in markets:
|
||||||
|
by_cat[mkt.category] = by_cat.get(mkt.category, 0) + 1
|
||||||
log.info(
|
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,
|
len(markets), min_volume, max_days_to_resolution,
|
||||||
|
", ".join(f"{k}={v}" for k, v in sorted(by_cat.items())),
|
||||||
)
|
)
|
||||||
return markets
|
return markets
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ class BayesianStrategy:
|
|||||||
Returns None if no actionable opportunity.
|
Returns None if no actionable opportunity.
|
||||||
"""
|
"""
|
||||||
question_lower = market.question.lower()
|
question_lower = market.question.lower()
|
||||||
|
category = market.category # set by PolymarketClient
|
||||||
|
|
||||||
# Classify what kind of market this is
|
# Classify what kind of market this is
|
||||||
is_price_above = any(w in question_lower for w in ["above", "over", "exceed", "higher", "atleast", "reach"])
|
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(
|
is_macro = any(
|
||||||
w in question_lower for w in ["nasdaq", "s&p", "sp500", "inflation", "fed rate", "interest rate", "tariff"]
|
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:
|
if not is_any_supported:
|
||||||
log.debug("Skipping unsupported market: %s", market.question[:60])
|
log.debug("Skipping unsupported market: %s", market.question[:60])
|
||||||
return None
|
return None
|
||||||
@@ -103,12 +110,17 @@ class BayesianStrategy:
|
|||||||
adjustments: list[float] = []
|
adjustments: list[float] = []
|
||||||
|
|
||||||
# Signal 1: Price momentum (asset-specific or total market cap as proxy)
|
# 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:
|
if is_btc:
|
||||||
momentum = ext.btc_change_24h
|
momentum = ext.btc_change_24h
|
||||||
asset_label = "BTC"
|
asset_label = "BTC"
|
||||||
elif is_eth:
|
elif is_eth:
|
||||||
momentum = ext.eth_change_24h
|
momentum = ext.eth_change_24h
|
||||||
asset_label = "ETH"
|
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:
|
else:
|
||||||
# Altcoins and general crypto: use total market cap change as proxy
|
# Altcoins and general crypto: use total market cap change as proxy
|
||||||
momentum = ext.total_market_cap_change
|
momentum = ext.total_market_cap_change
|
||||||
@@ -116,6 +128,9 @@ class BayesianStrategy:
|
|||||||
|
|
||||||
if abs(momentum) > 2:
|
if abs(momentum) > 2:
|
||||||
momentum_adj = math.tanh(momentum / 20) * 0.15 # Max ±15%
|
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)
|
adjustments.append(momentum_adj if is_price_above else -momentum_adj)
|
||||||
sources.append(f"{asset_label} 24h: {momentum:+.1f}%")
|
sources.append(f"{asset_label} 24h: {momentum:+.1f}%")
|
||||||
|
|
||||||
@@ -143,9 +158,11 @@ 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)")
|
||||||
|
|
||||||
# Macro markets: lower weight, rely only on Fear&Greed signal already added
|
# Macro/politics/tech/events: cap confidence lower to reflect weaker signal quality
|
||||||
# Cap confidence below for macro to reflect weaker signal quality
|
if is_macro or is_politics or is_tech or is_events:
|
||||||
confidence_cap = 0.70 if is_macro else 0.90
|
confidence_cap = 0.65
|
||||||
|
else:
|
||||||
|
confidence_cap = 0.90
|
||||||
|
|
||||||
# Compute posterior using log-odds updating
|
# Compute posterior using log-odds updating
|
||||||
log_odds_prior = math.log(prior / (1 - prior))
|
log_odds_prior = math.log(prior / (1 - prior))
|
||||||
|
|||||||
Reference in New Issue
Block a user