feat: Implement Telegram push notifications for weather and market conditions and a Polymarket read-only data interface.

This commit is contained in:
2569718930@qq.com
2026-03-11 02:13:38 +08:00
parent d6434bf174
commit 4e6cb1071c
2 changed files with 51 additions and 26 deletions
+43 -5
View File
@@ -813,11 +813,10 @@ class PolymarketReadOnlyLayer:
if not city_hit: if not city_hit:
return 0.0 return 0.0
score = 40.0 if not self._is_temperature_market(market):
weather_hit = any(_contains_token(haystack, keyword) for keyword in WEATHER_KEYWORDS)
if not weather_hit:
return 0.0 return 0.0
score = 40.0
score += 18.0 score += 18.0
d_target = _parse_target_date(target_date) d_target = _parse_target_date(target_date)
@@ -874,6 +873,42 @@ class PolymarketReadOnlyLayer:
score += min(volume / 50000.0, 8.0) score += min(volume / 50000.0, 8.0)
return score 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]: def _extract_market_date(self, market: Dict[str, Any]) -> Optional[str]:
for key in ( for key in (
"endDate", "endDate",
@@ -1221,6 +1256,10 @@ class PolymarketReadOnlyLayer:
] ]
] = [] ] = []
for market in candidate_markets: 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) tokens = self._extract_market_tokens(market)
yes_token, no_token = self._resolve_yes_no_tokens(tokens) yes_token, no_token = self._resolve_yes_no_tokens(tokens)
if not yes_token or not no_token: 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: if no_sell is None and yes_sell is not None:
no_sell = max(0.0, min(1.0, 1.0 - yes_sell)) 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() market_slug = str(market.get("slug") or "").strip()
top_rows.append( top_rows.append(
+8 -21
View File
@@ -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"): if not isinstance(market, dict) or not market.get("available"):
return True 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 {} forecast_bucket = market.get("forecast_bucket") or {}
yes_buy = None yes_buy = None
bucket_label = 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")) yes_buy = _norm_prob(forecast_bucket.get("yes_buy"))
bucket_label = str(forecast_bucket.get("label") or "").strip() or None bucket_label = str(forecast_bucket.get("label") or "").strip() or None
# Backward-compatible fallback.
if yes_buy is None: if yes_buy is None:
yes_buy = _norm_prob(market.get("yes_buy")) logger.info(
if not bucket_label: "trade alert skipped: no mapped forecast bucket city={} om_settle={}".format(
bucket_label = str(market.get("selected_bucket") or "").strip() or None alert_payload.get("city"),
market.get("open_meteo_settlement"),
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 [] return False
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
if yes_buy >= max_yes_buy: if yes_buy >= max_yes_buy:
logger.info( logger.info(