diff --git a/src/data_collection/polymarket_readonly.py b/src/data_collection/polymarket_readonly.py index 82491b1a..d1c87e89 100644 --- a/src/data_collection/polymarket_readonly.py +++ b/src/data_collection/polymarket_readonly.py @@ -813,11 +813,10 @@ class PolymarketReadOnlyLayer: if not city_hit: return 0.0 - score = 40.0 - - weather_hit = any(_contains_token(haystack, keyword) for keyword in WEATHER_KEYWORDS) - if not weather_hit: + if not self._is_temperature_market(market): return 0.0 + + score = 40.0 score += 18.0 d_target = _parse_target_date(target_date) @@ -874,6 +873,42 @@ class PolymarketReadOnlyLayer: score += min(volume / 50000.0, 8.0) return score + def _is_temperature_market(self, market: Dict[str, Any]) -> bool: + text_parts = [ + market.get("question"), + market.get("title"), + market.get("slug"), + market.get("eventSlug"), + market.get("description"), + ] + raw_text = " ".join(str(part or "") for part in text_parts) + if not raw_text: + return False + + # Hard signal: contains explicit Celsius bucket text like "10C" / "10°C" + if re.search(r"(-?\d+(?:\.\d+)?)\s*[°º]?\s*c\b", raw_text, re.IGNORECASE): + return True + + text = _normalize_text(raw_text) + if not text: + return False + + # Weather temperature event patterns. + if "highest temperature" in text: + return True + if "temperature in" in text: + return True + if "high temperature" in text: + return True + + # Conservative fallback: must explicitly mention temperature and boundary wording. + if "temperature" in text and any( + key in text for key in ("or higher", "or above", "or lower", "or below", "and above", "and below") + ): + return True + + return False + def _extract_market_date(self, market: Dict[str, Any]) -> Optional[str]: for key in ( "endDate", @@ -1221,6 +1256,10 @@ class PolymarketReadOnlyLayer: ] ] = [] for market in candidate_markets: + bucket_temp = self._extract_market_bucket_temp(market) + if bucket_temp is None: + continue + tokens = self._extract_market_tokens(market) yes_token, no_token = self._resolve_yes_no_tokens(tokens) if not yes_token or not no_token: @@ -1289,7 +1328,6 @@ class PolymarketReadOnlyLayer: if no_sell is None and yes_sell is not None: no_sell = max(0.0, min(1.0, 1.0 - yes_sell)) - bucket_temp = self._extract_market_bucket_temp(market) market_slug = str(market.get("slug") or "").strip() top_rows.append( diff --git a/src/utils/telegram_push.py b/src/utils/telegram_push.py index 4a8a8edd..b6a451d3 100644 --- a/src/utils/telegram_push.py +++ b/src/utils/telegram_push.py @@ -141,7 +141,7 @@ def _market_price_cap_ok(alert_payload: Dict[str, Any], max_yes_buy: float) -> b if not isinstance(market, dict) or not market.get("available"): return True - # Prefer the market bucket that maps to Open-Meteo forecast settlement. + # Strict rule: use the bucket mapped from Open-Meteo settlement. forecast_bucket = market.get("forecast_bucket") or {} yes_buy = None bucket_label = None @@ -149,27 +149,14 @@ def _market_price_cap_ok(alert_payload: Dict[str, Any], max_yes_buy: float) -> b yes_buy = _norm_prob(forecast_bucket.get("yes_buy")) bucket_label = str(forecast_bucket.get("label") or "").strip() or None - # Backward-compatible fallback. if yes_buy is None: - yes_buy = _norm_prob(market.get("yes_buy")) - if not bucket_label: - bucket_label = str(market.get("selected_bucket") or "").strip() or None - - if yes_buy is None: - # Fallback to first bucket with valid yes_buy if aggregate field is missing. - top_rows = market.get("top_bucket_rows") or [] - if isinstance(top_rows, list): - for row in top_rows: - if not isinstance(row, dict): - continue - yes_buy = _norm_prob(row.get("yes_buy")) - if yes_buy is not None: - if not bucket_label: - bucket_label = str(row.get("label") or "").strip() or None - break - - if yes_buy is None: - return True + logger.info( + "trade alert skipped: no mapped forecast bucket city={} om_settle={}".format( + alert_payload.get("city"), + market.get("open_meteo_settlement"), + ) + ) + return False if yes_buy >= max_yes_buy: logger.info(