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:
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(
+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"):
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(