feat: Implement PolyWeather web map API with integrated weather data analysis, market alert engine, and Telegram push utility.

This commit is contained in:
2569718930@qq.com
2026-03-11 12:15:09 +08:00
parent e89fdb7e09
commit db3b1c995a
3 changed files with 73 additions and 29 deletions
+55 -13
View File
@@ -511,6 +511,31 @@ def _extract_open_meteo_today_high_c(city_weather: Dict[str, Any]) -> Optional[f
return _to_celsius(om_today, temp_symbol)
def _extract_multi_model_anchor_high_c(
city_weather: Dict[str, Any],
) -> Tuple[Optional[float], Optional[str]]:
multi_model = city_weather.get("multi_model") or {}
temp_symbol = str(city_weather.get("temp_symbol") or "")
if isinstance(multi_model, dict):
anchor_model: Optional[str] = None
anchor_high_c: Optional[float] = None
for model_name, raw_value in multi_model.items():
value = _to_celsius(_sf(raw_value), temp_symbol)
if value is None:
continue
if anchor_high_c is None or value > anchor_high_c:
anchor_high_c = value
anchor_model = str(model_name or "").strip() or None
if anchor_high_c is not None:
return anchor_high_c, anchor_model
# Fallback keeps behavior resilient when multi-model data is unexpectedly missing.
fallback_high_c = _extract_open_meteo_today_high_c(city_weather)
if fallback_high_c is not None:
return fallback_high_c, "Open-Meteo"
return None, None
def _bucket_value(row: Dict[str, Any]) -> Optional[float]:
for key in ("value", "temp"):
value = _sf(row.get(key))
@@ -689,12 +714,12 @@ def _extract_market_snapshot(city_weather: Dict[str, Any]) -> Dict[str, Any]:
if slug:
market_url = f"https://polymarket.com/market/{slug}"
open_meteo_today_high_c = _extract_open_meteo_today_high_c(city_weather)
open_meteo_settlement = wu_round(open_meteo_today_high_c)
anchor_today_high_c, anchor_model = _extract_multi_model_anchor_high_c(city_weather)
anchor_settlement = wu_round(anchor_today_high_c)
forecast_bucket = _pick_bucket_for_forecast(
rows=all_bucket_rows,
forecast_settlement=open_meteo_settlement,
forecast_today_high_c=open_meteo_today_high_c,
forecast_settlement=anchor_settlement,
forecast_today_high_c=anchor_today_high_c,
)
forecast_market_url = None
if isinstance(forecast_bucket, dict):
@@ -717,8 +742,12 @@ def _extract_market_snapshot(city_weather: Dict[str, Any]) -> Dict[str, Any]:
"confidence": scan.get("confidence"),
"top_bucket_rows": top_bucket_rows,
"all_bucket_rows": all_bucket_rows,
"open_meteo_today_high_c": open_meteo_today_high_c,
"open_meteo_settlement": open_meteo_settlement,
"anchor_today_high_c": anchor_today_high_c,
"anchor_settlement": anchor_settlement,
"anchor_model": anchor_model,
# Backward-compatible aliases for existing consumers.
"open_meteo_today_high_c": anchor_today_high_c,
"open_meteo_settlement": anchor_settlement,
"forecast_bucket": forecast_bucket,
"primary_market_url": market_url,
"market_url": forecast_market_url or market_url,
@@ -996,8 +1025,13 @@ def _build_telegram_messages_mispricing(
f"({int(delta_min)}min 内 {delta_temp:+.1f}{temp_symbol}) {momentum_emoji}"
)
om_high_c = _sf(snapshot.get("open_meteo_today_high_c"))
om_settle = snapshot.get("open_meteo_settlement")
anchor_high_c = _sf(snapshot.get("anchor_today_high_c"))
if anchor_high_c is None:
anchor_high_c = _sf(snapshot.get("open_meteo_today_high_c"))
anchor_settle = snapshot.get("anchor_settlement")
if anchor_settle is None:
anchor_settle = snapshot.get("open_meteo_settlement")
anchor_model = str(snapshot.get("anchor_model") or "").strip()
forecast_bucket = snapshot.get("forecast_bucket") or {}
match_bucket_label = str(forecast_bucket.get("label") or "--").strip() or "--"
match_bucket_yes_prob = _norm_probability(forecast_bucket.get("yes_buy"))
@@ -1014,12 +1048,17 @@ def _build_telegram_messages_mispricing(
lines_zh = [f"🚨 PolyWeather 错价雷达 [{city_name}]"]
lines_zh.append("")
if om_high_c is not None and om_settle is not None:
lines_zh.append(
f"基准:Open-Meteo 今日高温 {om_high_c:.1f}C(结算参考 {om_settle}C"
)
if anchor_high_c is not None and anchor_settle is not None:
if anchor_model:
lines_zh.append(
f"基准:多模型最高温 {anchor_model} {anchor_high_c:.1f}C(结算参考 {anchor_settle}C"
)
else:
lines_zh.append(
f"基准:多模型最高温 {anchor_high_c:.1f}C(结算参考 {anchor_settle}C"
)
else:
lines_zh.append("基准:Open-Meteo 今日高温 --(结算参考 --")
lines_zh.append("基准:多模型最高温 --(结算参考 --")
lines_zh.append(f"命中桶:{match_bucket_label} | Yes: {match_bucket_yes}")
lines_zh.append("触发:该桶 Yes 价格 < 10c,疑似低估")
lines_zh.append("")
@@ -1176,6 +1215,9 @@ def _build_alert_evidence(
"confidence": market_snapshot.get("confidence"),
"top_bucket": market_snapshot.get("top_bucket"),
"top_bucket_prob": market_snapshot.get("top_bucket_prob"),
"anchor_today_high_c": market_snapshot.get("anchor_today_high_c"),
"anchor_settlement": market_snapshot.get("anchor_settlement"),
"anchor_model": market_snapshot.get("anchor_model"),
"open_meteo_today_high_c": market_snapshot.get("open_meteo_today_high_c"),
"open_meteo_settlement": market_snapshot.get("open_meteo_settlement"),
"forecast_bucket": {
+11 -5
View File
@@ -152,8 +152,12 @@ def _market_price_cap_ok(
return False
return True
# Strict rule: use the bucket mapped from Open-Meteo settlement.
# Strict rule: use the bucket mapped from multi-model anchor settlement.
forecast_bucket = market.get("forecast_bucket") or {}
settle_ref = market.get("anchor_settlement")
if settle_ref is None:
settle_ref = market.get("open_meteo_settlement")
anchor_model = str(market.get("anchor_model") or "").strip() or "--"
yes_buy = None
bucket_label = None
if isinstance(forecast_bucket, dict):
@@ -162,20 +166,22 @@ def _market_price_cap_ok(
if yes_buy is None or yes_buy <= 0.0:
logger.info(
"trade alert skipped: no actionable mapped bucket quote city={} bucket={} om_settle={}".format(
"trade alert skipped: no actionable mapped bucket quote city={} bucket={} anchor_model={} anchor_settle={}".format(
alert_payload.get("city"),
bucket_label or "--",
market.get("open_meteo_settlement"),
anchor_model,
settle_ref,
)
)
return False
if yes_buy >= max_yes_buy:
logger.info(
"trade alert skipped by mispricing cap city={} bucket={} om_settle={} yes_buy={} cap={}".format(
"trade alert skipped by mispricing cap city={} bucket={} anchor_model={} anchor_settle={} yes_buy={} cap={}".format(
alert_payload.get("city"),
bucket_label or "--",
market.get("open_meteo_settlement"),
anchor_model,
settle_ref,
round(yes_buy, 4),
round(max_yes_buy, 4),
)
+7 -11
View File
@@ -802,23 +802,19 @@ def _build_city_detail_payload(
if not isinstance(distribution, list) or not distribution:
distribution = data.get("probabilities", {}).get("distribution", []) or []
city_name = str(data.get("name") or "").strip().lower()
model_map = selected_daily.get("models") or data.get("multi_model") or {}
if not isinstance(model_map, dict):
model_map = {}
# Mispricing anchor temperature:
# - Ankara: use MGM today-high forecast
# - Others: use Open-Meteo today-high forecast
# use the highest value across all available model highs.
anchor_temp = None
if city_name == "ankara":
anchor_temp = _sf(model_map.get("MGM"))
else:
anchor_temp = _sf(model_map.get("Open-Meteo"))
if anchor_temp is None and city_name == "ankara":
# Keep radar available when MGM is missing unexpectedly.
anchor_temp = _sf(model_map.get("Open-Meteo"))
for raw_value in model_map.values():
value = _sf(raw_value)
if value is None:
continue
if anchor_temp is None or value > anchor_temp:
anchor_temp = value
primary_bucket = None
if isinstance(distribution, list) and distribution: