From db3b1c995ac00752a146b75749848c1ba454f5d0 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Wed, 11 Mar 2026 12:15:09 +0800 Subject: [PATCH] feat: Implement PolyWeather web map API with integrated weather data analysis, market alert engine, and Telegram push utility. --- src/analysis/market_alert_engine.py | 68 +++++++++++++++++++++++------ src/utils/telegram_push.py | 16 ++++--- web/app.py | 18 +++----- 3 files changed, 73 insertions(+), 29 deletions(-) diff --git a/src/analysis/market_alert_engine.py b/src/analysis/market_alert_engine.py index 4df0f810..2ad32e69 100644 --- a/src/analysis/market_alert_engine.py +++ b/src/analysis/market_alert_engine.py @@ -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": { diff --git a/src/utils/telegram_push.py b/src/utils/telegram_push.py index 297a685e..aa8a092f 100644 --- a/src/utils/telegram_push.py +++ b/src/utils/telegram_push.py @@ -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), ) diff --git a/web/app.py b/web/app.py index 62d237da..cc0e8aff 100644 --- a/web/app.py +++ b/web/app.py @@ -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: