From 1958b2764b8135bb691eb47341e26e611d23fdd2 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Wed, 11 Mar 2026 10:49:35 +0800 Subject: [PATCH] feat: Implement core PolyWeather application with city weather query service, trend analysis, data collection, and dashboard UI. --- .env.example | 5 - config/config.yaml | 1 - docs/API_ZH.md | 1 - frontend/components/dashboard/GuideModal.tsx | 4 +- .../components/dashboard/PanelSections.tsx | 18 +-- frontend/lib/dashboard-types.ts | 6 - frontend/lib/dashboard-utils.ts | 57 +------ frontend/lib/i18n.ts | 4 +- frontend/lib/types.ts | 1 - frontend/public/legacy/index.html | 9 +- src/analysis/city_query_service.py | 31 +--- src/analysis/trend_engine.py | 3 - src/data_collection/polymarket_readonly.py | 13 +- src/data_collection/weather_sources.py | 141 ------------------ src/utils/config_loader.py | 1 - tests/test_trend_engine.py | 1 - web/app.py | 7 - 17 files changed, 24 insertions(+), 279 deletions(-) diff --git a/.env.example b/.env.example index d47084a9..efa6b1b6 100644 --- a/.env.example +++ b/.env.example @@ -13,10 +13,6 @@ TELEGRAM_ALERT_CITIES=ankara,london,paris,seoul,toronto,buenos aires,wellington, # AI GROQ_API_KEY=your_groq_api_key_here -# Meteoblue -METEOBLUE_API_KEY=your_meteoblue_api_key_here -METEOBLUE_CACHE_TTL_SEC=7200 - # Open-Meteo (forecast data changes ~hourly, no need to refresh more often) OPEN_METEO_CACHE_TTL_SEC=7200 OPEN_METEO_ENSEMBLE_CACHE_TTL_SEC=7200 @@ -76,4 +72,3 @@ POLYMARKET_WALLET_ACTIVITY_NOTIFY_CLOSED=false POLYMARKET_WALLET_ACTIVITY_BOOTSTRAP_ALERT=false POLYMARKET_WALLET_ACTIVITY_AVG_PRICE_SHOW_MIN=0.01 POLYMARKET_WALLET_ACTIVITY_AVG_PRICE_SHOW_MAX=0.99 - diff --git a/config/config.yaml b/config/config.yaml index 4ad96862..67bb35d4 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -1,6 +1,5 @@ # Weather API Configuration weather: - meteoblue_api_key: null # Set via METEOBLUE_API_KEY env var timeout: 30 # Target Cities diff --git a/docs/API_ZH.md b/docs/API_ZH.md index a35e24f6..444f49a5 100644 --- a/docs/API_ZH.md +++ b/docs/API_ZH.md @@ -211,7 +211,6 @@ - **Open-Meteo** - **weather.gov**(美国城市) -- **Meteoblue**(部分城市) - **多模型集成**: ECMWF / GFS / ICON / GEM / JMA --- diff --git a/frontend/components/dashboard/GuideModal.tsx b/frontend/components/dashboard/GuideModal.tsx index e61ea63c..0a33cf30 100644 --- a/frontend/components/dashboard/GuideModal.tsx +++ b/frontend/components/dashboard/GuideModal.tsx @@ -22,7 +22,7 @@ const GUIDE_CARDS = { title: "Ankara 专属增强", }, { - body: "点击多日预报后的模态框,主要用于分析下一个交易日。6-48 小时趋势以 weather.gov 和 Open-Meteo 为主,部分城市补充 Meteoblue;0-2 小时临近判断优先看 METAR 与周边站。", + body: "点击多日预报后的模态框,主要用于分析下一个交易日。6-48 小时趋势以 weather.gov 和 Open-Meteo 为主;0-2 小时临近判断优先看 METAR 与周边站。", title: "未来日期分析", }, { @@ -48,7 +48,7 @@ const GUIDE_CARDS = { title: "Ankara-specific Enhancement", }, { - body: "The multi-day modal focuses on next-session analysis. 6-48h trend mainly relies on weather.gov and Open-Meteo, with Meteoblue in selected cities; 0-2h nowcast prioritizes METAR and nearby stations.", + body: "The multi-day modal focuses on next-session analysis. 6-48h trend mainly relies on weather.gov and Open-Meteo; 0-2h nowcast prioritizes METAR and nearby stations.", title: "Future-date Analysis", }, { diff --git a/frontend/components/dashboard/PanelSections.tsx b/frontend/components/dashboard/PanelSections.tsx index 2d906019..229131b2 100644 --- a/frontend/components/dashboard/PanelSections.tsx +++ b/frontend/components/dashboard/PanelSections.tsx @@ -552,18 +552,8 @@ export function ModelForecast({ }) { const { locale, t } = useI18n(); const view = getModelView(detail, targetDate); - const fallbackMeteoblue = detail.source_forecasts?.meteoblue?.today_high; const modelsMap = { ...view.models }; - // 强行插入 Meteoblue,防止 helper 函数或日期对齐导致其被剔除 - if ( - modelsMap["Meteoblue"] == null && - fallbackMeteoblue != null && - (!targetDate || targetDate === detail.local_date) - ) { - modelsMap["Meteoblue"] = fallbackMeteoblue; - } - const modelEntries = Object.entries(modelsMap).filter( ([, value]) => value !== null && value !== undefined && Number.isFinite(Number(value)), @@ -591,12 +581,6 @@ export function ModelForecast({ ? Math.max(...comparisonValues) + 1 : 1; const range = Math.max(maxValue - minValue, 1); - const getModelDisplayName = (name: string) => { - if (String(name).trim().toLowerCase() === "meteoblue") { - return "Meteoblue"; - } - return name; - }; return (
@@ -615,7 +599,7 @@ export function ModelForecast({ return (
- {getModelDisplayName(name)} + {name}
; - unit?: string | null; - url?: string | null; - }; } export interface DailyModelForecast { diff --git a/frontend/lib/dashboard-utils.ts b/frontend/lib/dashboard-utils.ts index 3583090b..8eb42ba5 100644 --- a/frontend/lib/dashboard-utils.ts +++ b/frontend/lib/dashboard-utils.ts @@ -330,67 +330,18 @@ export function getProbabilityView(detail: CityDetail, targetDate?: string | nul } export function getModelView(detail: CityDetail, targetDate?: string | null) { - const toFiniteNumber = (value: unknown): number | null => { - if (value === null || value === undefined || value === "") return null; - const numeric = Number(value); - return Number.isFinite(numeric) ? numeric : null; - }; - - const pickMeteoblueForDate = (dateStr: string) => { - const meteoblue = detail.source_forecasts?.meteoblue; - if (!meteoblue) return null; - - const dates = detail.forecast?.daily?.map((item) => item.date) || []; - const index = dates.findIndex((date) => date === dateStr); - const todayHigh = toFiniteNumber(meteoblue.today_high); - - // Today must always use Meteoblue daily max (today_high) first. - if (dateStr === detail.local_date && todayHigh != null) { - return todayHigh; - } - - const dailyHighs = Array.isArray(meteoblue.daily_highs) - ? meteoblue.daily_highs - : []; - if (index >= 0) { - const byDailyHigh = toFiniteNumber(dailyHighs[index]); - if (byDailyHigh != null) return byDailyHigh; - } - - if (index === 0 && todayHigh != null) return todayHigh; - - return null; - }; - - const withMeteoblue = ( - models: Record, - dateStr: string, - ) => { - const nextModels = { ...models }; - const existing = toFiniteNumber(nextModels.Meteoblue); - if (existing == null) { - const meteoblueVal = pickMeteoblueForDate(dateStr); - if (meteoblueVal != null) { - nextModels.Meteoblue = meteoblueVal; - } - } else { - nextModels.Meteoblue = existing; - } - return nextModels; - }; - const date = targetDate || detail.local_date; const daily = detail.multi_model_daily?.[date]; if (daily) { return { deb: daily.deb?.prediction ?? null, - models: withMeteoblue(daily.models || {}, date), + models: daily.models || {}, }; } return { deb: detail.deb?.prediction ?? null, - models: withMeteoblue(detail.multi_model || {}, date), + models: detail.multi_model || {}, }; } @@ -712,10 +663,6 @@ export function computeFrontTrendSignal( ? isEnglish(locale) ? "Temperature declines with pressure rebound and/or northerly shift. Next 6-48h leans cold-front suppression." : "温度下滑、气压回升或风向转北,未来 6-48 小时更像冷锋或冷平流压制。" - : detail.name !== "ankara" && Boolean(detail.source_forecasts?.meteoblue) - ? isEnglish(locale) - ? "Structured trend layer mainly uses weather.gov, Open-Meteo and Meteoblue for 6-48h warm/cold flow judgement." - : "结构化来源以 weather.gov、Open-Meteo、Meteoblue 为主,用于判断未来 6-48 小时冷暖平流趋势。" : isEnglish(locale) ? "Structured trend layer mainly uses weather.gov and Open-Meteo for 6-48h warm/cold flow judgement." : "结构化来源以 weather.gov 和 Open-Meteo 为主,用于判断未来 6-48 小时冷暖平流趋势。", diff --git a/frontend/lib/i18n.ts b/frontend/lib/i18n.ts index 0564db33..fe0415c2 100644 --- a/frontend/lib/i18n.ts +++ b/frontend/lib/i18n.ts @@ -42,7 +42,7 @@ const MESSAGES: Record> = { "guide.title": "📎 PolyWeather 系统技术说明", "guide.closeAria": "关闭技术说明", "guide.footer": - "数据源以 Aviation Weather / METAR、Turkish MGM、Open-Meteo、weather.gov 为主,部分城市补充 Meteoblue。", + "数据源以 Aviation Weather / METAR、Turkish MGM、Open-Meteo、weather.gov 为主。", "history.title": "📊 历史准确率对账 - {city}", "history.closeAria": "关闭历史对账", @@ -143,7 +143,7 @@ const MESSAGES: Record> = { "guide.title": "📎 PolyWeather Technical Overview", "guide.closeAria": "Close technical overview", "guide.footer": - "Primary data sources are Aviation Weather / METAR, Turkish MGM, Open-Meteo, and weather.gov, with Meteoblue added for selected cities.", + "Primary data sources are Aviation Weather / METAR, Turkish MGM, Open-Meteo, and weather.gov.", "history.title": "📊 Historical Reconciliation - {city}", "history.closeAria": "Close history reconciliation", diff --git a/frontend/lib/types.ts b/frontend/lib/types.ts index 4cd3d9c9..88f9a34e 100644 --- a/frontend/lib/types.ts +++ b/frontend/lib/types.ts @@ -186,7 +186,6 @@ export interface ModelComparison { JMA?: number; MGM?: number; NWS?: number; - Meteoblue?: number; } export interface DEBAnalysis { diff --git a/frontend/public/legacy/index.html b/frontend/public/legacy/index.html index f3a7fafb..3549510b 100644 --- a/frontend/public/legacy/index.html +++ b/frontend/public/legacy/index.html @@ -1,4 +1,4 @@ - + @@ -151,7 +151,7 @@

📆 未来日期分析

-

点击多日预报后打开的模态框,主要用于分析下一个交易日。 6-48 小时趋势以 weather.gov 和 Open-Meteo 为主,部分城市可会补充 Meteoblue; 0-2 小时临近判断则优先看 METAR 与周边站。

+

点击多日预报后打开的模态框,主要用于分析下一个交易日。 6-48 小时趋势以 weather.gov 和 Open-Meteo 为主; 0-2 小时临近判断则优先看 METAR 与周边站。

📊 历史对账规则

@@ -159,7 +159,7 @@
@@ -236,6 +236,3 @@ - - - diff --git a/src/analysis/city_query_service.py b/src/analysis/city_query_service.py index 1e74df69..0dcc870e 100644 --- a/src/analysis/city_query_service.py +++ b/src/analysis/city_query_service.py @@ -175,7 +175,6 @@ def _append_future_forecast_lines( mgm_daily[date_key] = day_high mm_raw = weather_data.get("multi_model") or {} mm_daily = mm_raw.get("daily_forecasts", {}) if isinstance(mm_raw, dict) else {} - mb_daily = (weather_data.get("meteoblue") or {}).get("daily_highs", []) or [] nws_periods = (weather_data.get("nws") or {}).get("forecast_periods", []) or [] if len(dates) > 1: @@ -231,18 +230,6 @@ def _append_future_forecast_lines( lines.append("📅 " + " | ".join(future)) return - if isinstance(mb_daily, list) and len(mb_daily) > 1: - future = [] - for idx in range(1, min(3, len(mb_daily))): - day_temp = _sf(mb_daily[idx]) - if day_temp is None: - continue - day = (local_now + timedelta(days=idx)).strftime("%m-%d") - future.append(f"{day}: MB {day_temp:.1f}{temp_symbol}") - if future: - lines.append("📅 " + " | ".join(future)) - return - if isinstance(nws_periods, list) and nws_periods: future = [] seen_days = set() @@ -358,14 +345,13 @@ def build_city_query_report( nws_high = _sf((weather_data.get("nws") or {}).get("today_high")) mgm_high = _sf((mgm.get("today_high") if isinstance(mgm, dict) else None)) - mb_high = _sf((weather_data.get("meteoblue") or {}).get("today_high")) metar_max_so_far = _sf((metar.get("current") or {}).get("max_temp_so_far")) today_t = _sf(max_temps[0]) if max_temps else None fallback_source = None metar_only_fallback = False if today_t is None: - for source_name, candidate in (("MB", mb_high), ("NWS", nws_high), ("MGM", mgm_high)): + for source_name, candidate in (("NWS", nws_high), ("MGM", mgm_high)): if candidate is not None: today_t = candidate fallback_source = source_name @@ -378,11 +364,6 @@ def build_city_query_report( sources = ["Open-Meteo"] if max_temps else [] comp_parts: List[str] = [] - if mb_high is not None: - if "MB" not in sources: - sources.append("MB") - if fallback_source != "MB": - comp_parts.append(f"MB: {mb_high:.1f}{temp_symbol}") if nws_high is not None: if "NWS" not in sources: sources.append("NWS") @@ -402,18 +383,10 @@ def build_city_query_report( if not sources: sources = ["N/A"] - divergence_warning = "" - base_for_divergence = _sf(max_temps[0]) if max_temps else today_t - if mb_high is not None and base_for_divergence is not None: - diff = abs(mb_high - base_for_divergence) - threshold = 5.0 if city_is_fahrenheit else 2.5 - if diff > threshold: - divergence_warning = f" ⚠️ 模型显著分歧 ({diff:.1f}{temp_symbol})" - comp_str = f" ({' | '.join(comp_parts)})" if comp_parts else "" msg_lines.append(f"\n📊 预报 ({' | '.join(sources)})") msg_lines.append( - f"👉 今天: {today_t_display}{temp_symbol}{comp_str}{divergence_warning}" + f"👉 今天: {today_t_display}{temp_symbol}{comp_str}" ) _append_future_forecast_lines( diff --git a/src/analysis/trend_engine.py b/src/analysis/trend_engine.py index fec3a4ac..827592b9 100644 --- a/src/analysis/trend_engine.py +++ b/src/analysis/trend_engine.py @@ -62,7 +62,6 @@ def analyze_weather_trend( metar = weather_data.get("metar", {}) open_meteo = weather_data.get("open-meteo", {}) mgm = weather_data.get("mgm") or {} - mb = weather_data.get("meteoblue", {}) nws = weather_data.get("nws", {}) empty_result = ("", "", {}) @@ -81,8 +80,6 @@ def analyze_weather_trend( current_forecasts: Dict[str, Optional[float]] = {} if daily.get("temperature_2m_max"): current_forecasts["Open-Meteo"] = _sf(daily.get("temperature_2m_max")[0]) - if mb.get("today_high") is not None: - current_forecasts["Meteoblue"] = _sf(mb.get("today_high")) if nws.get("today_high") is not None: current_forecasts["NWS"] = _sf(nws.get("today_high")) diff --git a/src/data_collection/polymarket_readonly.py b/src/data_collection/polymarket_readonly.py index 089947cd..8868c3f8 100644 --- a/src/data_collection/polymarket_readonly.py +++ b/src/data_collection/polymarket_readonly.py @@ -1263,6 +1263,7 @@ class PolymarketReadOnlyLayer: ranked: List[ Tuple[ + float, float, float, Dict[str, Any], @@ -1315,6 +1316,7 @@ class PolymarketReadOnlyLayer: ( market_prob, volume, + bucket_temp, market, yes_token, no_token, @@ -1331,7 +1333,16 @@ class PolymarketReadOnlyLayer: max_items = max(1, int(limit or 4)) primary_slug = str(primary_market.get("slug") or "").strip().lower() - for market_prob, _volume, market, yes_token, no_token, yes_prices, no_prices in ranked[ + for ( + market_prob, + _volume, + bucket_temp, + market, + yes_token, + no_token, + yes_prices, + no_prices, + ) in ranked[ :max_items ]: yes_buy = _extract_price(yes_prices.get("buy")) diff --git a/src/data_collection/weather_sources.py b/src/data_collection/weather_sources.py index 0ffd71db..75999e04 100644 --- a/src/data_collection/weather_sources.py +++ b/src/data_collection/weather_sources.py @@ -42,25 +42,10 @@ class WeatherDataCollector: "munich": ["EDDM", "EDMO", "EDJA"], } - # Meteoblue 仅在增益最大的城市启用(减少配额消耗与冗余请求) - METEOBLUE_PRIORITY_CITIES = { - "ankara", - "london", - "paris", - "seoul", - "toronto", - "buenos aires", - "wellington", - "lucknow", - "sao paulo", - "munich", - } - def __init__(self, config: dict): self.config = config weather_cfg = config.get("weather", {}) self.wunderground_key = weather_cfg.get("wunderground_api_key") - self.meteoblue_key = weather_cfg.get("meteoblue_api_key") self.timeout = 30 # 增加超时以支持高延迟 VPS self.session = requests.Session() @@ -91,11 +76,6 @@ class WeatherDataCollector: ) self._open_meteo_last_call_ts: float = 0.0 self._open_meteo_call_lock = threading.Lock() - self.meteoblue_cache_ttl_sec = int( - os.getenv("METEOBLUE_CACHE_TTL_SEC", "7200") - ) - self._meteoblue_cache: Dict[str, Dict] = {} - self._meteoblue_cache_lock = threading.Lock() self.metar_cache_ttl_sec = int( os.getenv("METAR_CACHE_TTL_SEC", "600") # 默认 10 分钟 ) @@ -1558,105 +1538,6 @@ class WeatherDataCollector: return fallback return None - def fetch_from_meteoblue( - self, - lat: float, - lon: float, - timezone_name: str = "UTC", - use_fahrenheit: bool = False, - ) -> Optional[Dict]: - """ - 通过 Meteoblue 官方 API 获取高精度预测数据 - 带本地缓存,避免频繁请求触发 429。 - """ - if not self.meteoblue_key: - logger.warning("Meteoblue API Key 未配置,跳过抓取。") - return None - - cache_key = f"{round(float(lat), 4)}:{round(float(lon), 4)}:{'f' if use_fahrenheit else 'c'}" - now_ts = time.time() - with self._meteoblue_cache_lock: - cached = self._meteoblue_cache.get(cache_key) - if ( - cached - and now_ts - float(cached.get("t", 0)) < self.meteoblue_cache_ttl_sec - ): - cached_data = cached.get("data") - if isinstance(cached_data, dict): - return dict(cached_data) - - try: - # 1. 调用官方 API (使用 basic-day 包,它是多模型 ML 融合结果) - # 格式: https://my.meteoblue.com/packages/basic-day?apikey=KEY&lat=LAT&lon=LON&format=json - url = "https://my.meteoblue.com/packages/basic-day" - params = { - "apikey": self.meteoblue_key, - "lat": lat, - "lon": lon, - "format": "json", - "as_daylight": "true", - } - - response = self.session.get(url, params=params, timeout=self.timeout) - response.raise_for_status() - data = response.json() - - day_data = data.get("data_day", {}) - max_temps = day_data.get("temperature_max", []) - - if not max_temps: - logger.warning( - f"Meteoblue API 返回数据中找不到最高温 (坐标: {lat},{lon})" - ) - return None - - # 2. 转换单位 - def c_to_f(c): - return round((c * 9 / 5) + 32, 1) - - result = { - "source": "meteoblue", - "today_high": None, - "daily_highs": [], - "unit": "fahrenheit" if use_fahrenheit else "celsius", - "url": f"https://www.meteoblue.com/en/weather/week/{lat}N{lon}E", # 仅供参考 - } - - # 提取今日最高 - mb_today_c = max_temps[0] - result["today_high"] = c_to_f(mb_today_c) if use_fahrenheit else mb_today_c - - # 提取接下来几天的最高温 - if use_fahrenheit: - result["daily_highs"] = [c_to_f(t) for t in max_temps] - else: - result["daily_highs"] = max_temps - - with self._meteoblue_cache_lock: - self._meteoblue_cache[cache_key] = { - "t": now_ts, - "data": dict(result), - } - - logger.info( - f"✅ Meteoblue API 获取成功 ({lat},{lon}): 今天 {result['today_high']}{result['unit']}" - ) - return result - except Exception as e: - status_code = getattr(getattr(e, "response", None), "status_code", None) - if status_code == 429: - logger.warning("Meteoblue API 限流(429),尝试使用本地缓存回退。") - else: - logger.error(f"Meteoblue API fetch failed: {e}") - - with self._meteoblue_cache_lock: - stale = self._meteoblue_cache.get(cache_key) - if stale and isinstance(stale.get("data"), dict): - fallback = dict(stale["data"]) - fallback["stale_cache"] = True - return fallback - return None - def extract_date_from_title(self, title: str) -> Optional[str]: """ 从标题中提取日期并标准化为 YYYY-MM-DD @@ -1836,7 +1717,6 @@ class WeatherDataCollector: unit = "f" if use_fahrenheit else "c" open_meteo_key = f"{base}:14:{unit}" ensemble_key = f"{base}:{unit}" - meteoblue_key = ensemble_key multi_model_key = ensemble_key with self._open_meteo_cache_lock: @@ -1845,8 +1725,6 @@ class WeatherDataCollector: self._ensemble_cache.pop(ensemble_key, None) with self._multi_model_cache_lock: self._multi_model_cache.pop(multi_model_key, None) - with self._meteoblue_cache_lock: - self._meteoblue_cache.pop(meteoblue_key, None) icao = self.get_icao_code(city) if icao: @@ -1962,16 +1840,6 @@ class WeatherDataCollector: # 获取时区偏移以过滤 METAR utc_offset = open_meteo.get("utc_offset", 0) - if city_lower in self.METEOBLUE_PRIORITY_CITIES: - mb_data = self.fetch_from_meteoblue( - lat, - lon, - timezone_name=open_meteo.get("timezone", "UTC"), - use_fahrenheit=use_fahrenheit, - ) - if mb_data: - results["meteoblue"] = mb_data - # 对美国城市,额外获取 NWS 高精预报 if use_fahrenheit: nws_data = self.fetch_nws(lat, lon) @@ -2023,15 +1891,6 @@ class WeatherDataCollector: if cluster_data: results["mgm_nearby"] = cluster_data - if city_lower in self.METEOBLUE_PRIORITY_CITIES: - mb_data = self.fetch_from_meteoblue( - lat, - lon, - timezone_name="UTC", - use_fahrenheit=use_fahrenheit, - ) - if mb_data: - results["meteoblue"] = mb_data if use_fahrenheit: nws_data = self.fetch_nws(lat, lon) if nws_data: diff --git a/src/utils/config_loader.py b/src/utils/config_loader.py index 8d705009..5fbcd5c4 100644 --- a/src/utils/config_loader.py +++ b/src/utils/config_loader.py @@ -19,7 +19,6 @@ def load_config(): "openweather_api_key": get_env_or_none("OPENWEATHER_API_KEY"), "wunderground_api_key": get_env_or_none("WUNDERGROUND_API_KEY"), "visualcrossing_api_key": get_env_or_none("VISUALCROSSING_API_KEY"), - "meteoblue_api_key": get_env_or_none("METEOBLUE_API_KEY"), "proxy": os.getenv("HTTPS_PROXY") or os.getenv("HTTP_PROXY"), }, "telegram": { diff --git a/tests/test_trend_engine.py b/tests/test_trend_engine.py index c2fecc4e..4df77136 100644 --- a/tests/test_trend_engine.py +++ b/tests/test_trend_engine.py @@ -62,7 +62,6 @@ def _make_weather_data( "p90": ens_p90, }, "multi_model": {"forecasts": multi_model or {}}, - "meteoblue": {}, "nws": {}, } return data diff --git a/web/app.py b/web/app.py index a96f89d2..242a9dae 100644 --- a/web/app.py +++ b/web/app.py @@ -252,13 +252,10 @@ def _analyze(city: str, force_refresh: bool = False) -> Dict[str, Any]: forecast_daily = [{"date": d, "max_temp": t} for d, t in zip(dates, maxtemps)] if om_today is None: nws_high = _sf(raw.get("nws", {}).get("today_high")) - mb_high = _sf(raw.get("meteoblue", {}).get("today_high")) mgm_high = _sf(mgm.get("today_high")) if mgm else None fallback_high = ( nws_high if nws_high is not None - else mb_high - if mb_high is not None else mgm_high if mgm_high is not None else max_so_far @@ -291,9 +288,6 @@ def _analyze(city: str, force_refresh: bool = False) -> Dict[str, Any]: nws_high = _sf(raw.get("nws", {}).get("today_high")) if nws_high is not None: current_forecasts["NWS"] = nws_high - mb_high = _sf(raw.get("meteoblue", {}).get("today_high")) - if mb_high is not None: - current_forecasts["Meteoblue"] = mb_high mgm_high = _sf(mgm.get("today_high")) if mgm else None if mgm_high is not None: current_forecasts["MGM"] = mgm_high @@ -680,7 +674,6 @@ def _analyze(city: str, force_refresh: bool = False) -> Dict[str, Any]: }, "source_forecasts": { "weather_gov": raw.get("nws") or {}, - "meteoblue": raw.get("meteoblue") or {}, }, "multi_model": {k: v for k, v in current_forecasts.items() if v is not None}, "multi_model_daily": multi_model_daily,