diff --git a/frontend/components/dashboard/PanelSections.tsx b/frontend/components/dashboard/PanelSections.tsx index 7c12024d..c7c55200 100644 --- a/frontend/components/dashboard/PanelSections.tsx +++ b/frontend/components/dashboard/PanelSections.tsx @@ -550,7 +550,7 @@ export function ModelForecast({ hideTitle?: boolean; targetDate?: string | null; }) { - const { t } = useI18n(); + const { locale, t } = useI18n(); const view = getModelView(detail, targetDate); const modelEntries = Object.entries(view.models).filter(([, value]) => Number.isFinite(Number(value)), @@ -565,6 +565,14 @@ 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 locale === "en-US" + ? "Meteoblue (Daily Max)" + : "Meteoblue (日最高温)"; + } + return name; + }; return (
@@ -587,7 +595,7 @@ export function ModelForecast({ return (
- {name} + {getModelDisplayName(name)}
; + unit?: string | null; + url?: string | null; }; } diff --git a/frontend/lib/dashboard-utils.ts b/frontend/lib/dashboard-utils.ts index f11e49d1..ee71f40d 100644 --- a/frontend/lib/dashboard-utils.ts +++ b/frontend/lib/dashboard-utils.ts @@ -330,18 +330,58 @@ export function getProbabilityView(detail: CityDetail, targetDate?: string | nul } export function getModelView(detail: CityDetail, targetDate?: string | 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 dailyHighs = Array.isArray(meteoblue.daily_highs) + ? meteoblue.daily_highs + : []; + if (index >= 0) { + const byDailyHigh = Number(dailyHighs[index]); + if (Number.isFinite(byDailyHigh)) return byDailyHigh; + } + + const todayHigh = Number(meteoblue.today_high); + if ( + Number.isFinite(todayHigh) && + (dateStr === detail.local_date || index === 0) + ) { + return todayHigh; + } + + return null; + }; + + const withMeteoblue = ( + models: Record, + dateStr: string, + ) => { + const nextModels = { ...models }; + if (!Number.isFinite(Number(nextModels.Meteoblue))) { + const meteoblueVal = pickMeteoblueForDate(dateStr); + if (Number.isFinite(Number(meteoblueVal))) { + nextModels.Meteoblue = Number(meteoblueVal); + } + } + return nextModels; + }; + const date = targetDate || detail.local_date; const daily = detail.multi_model_daily?.[date]; if (daily) { return { deb: daily.deb?.prediction ?? null, - models: daily.models || {}, + models: withMeteoblue(daily.models || {}, date), }; } return { deb: detail.deb?.prediction ?? null, - models: detail.multi_model || {}, + models: withMeteoblue(detail.multi_model || {}, date), }; } diff --git a/src/analysis/market_alert_engine.py b/src/analysis/market_alert_engine.py index ae79141d..2ac3cdcf 100644 --- a/src/analysis/market_alert_engine.py +++ b/src/analysis/market_alert_engine.py @@ -5,6 +5,7 @@ Rule-based weather alert engine for short-horizon trading signals. from __future__ import annotations import math +import re from datetime import datetime, timezone from typing import Any, Dict, List, Optional @@ -454,13 +455,19 @@ def _bucket_label(bucket: Any) -> Optional[str]: or str(bucket.get("range") or "").strip() ) if direct: - return direct + normalized = re.sub( + r"(? Dict[str, Any]: } ) + market_url = None + websocket = scan.get("websocket") or {} + if isinstance(websocket, dict): + market_url = str(websocket.get("market_url") or "").strip() or None + if not market_url: + primary_market = scan.get("primary_market") or {} + if isinstance(primary_market, dict): + slug = str(primary_market.get("slug") or "").strip() + if slug: + market_url = f"https://polymarket.com/market/{slug}" + return { "available": True, "selected_bucket": _bucket_label(scan.get("temperature_bucket")), @@ -523,6 +541,7 @@ def _extract_market_snapshot(city_weather: Dict[str, Any]) -> Dict[str, Any]: "signal_label": scan.get("signal_label"), "confidence": scan.get("confidence"), "top_bucket_rows": top_bucket_rows, + "market_url": market_url, } @@ -672,7 +691,7 @@ def _build_telegram_messages( label = row.get("label") or "--" prob_text = _fmt_percent(row.get("probability")) yes_buy_text = _fmt_cents(row.get("yes_buy")) - lines_zh.append(f"{label} {prob_text} | 买Yes: {yes_buy_text}") + lines_zh.append(f"{label} {prob_text} | Yes: {yes_buy_text}") if market_snapshot.get("available") and not market_snapshot.get("top_bucket_rows"): market_edge = _sf(market_snapshot.get("edge_percent")) market_edge_text = f"{market_edge:+.1f}%" if market_edge is not None else "--" @@ -690,6 +709,8 @@ def _build_telegram_messages( f"市场最热桶:{market_snapshot.get('top_bucket')} " f"({_fmt_percent(market_snapshot.get('top_bucket_prob'))})" ) + if market_snapshot.get("market_url"): + lines_zh.append(f"市场链接:{market_snapshot.get('market_url')}") lines_zh.append(f"AI 建议:{advice}") lines_zh.append(f"点击查看实时地图:{final_map}") @@ -739,7 +760,7 @@ def _build_telegram_messages( label = row.get("label") or "--" prob_text = _fmt_percent(row.get("probability")) yes_buy_text = _fmt_cents(row.get("yes_buy")) - lines_en.append(f"{label} {prob_text} | Buy Yes: {yes_buy_text}") + lines_en.append(f"{label} {prob_text} | Yes: {yes_buy_text}") if market_snapshot.get("available") and not market_snapshot.get("top_bucket_rows"): market_edge = _sf(market_snapshot.get("edge_percent")) market_edge_text = f"{market_edge:+.1f}%" if market_edge is not None else "--" @@ -757,6 +778,8 @@ def _build_telegram_messages( f"Top market bucket: {market_snapshot.get('top_bucket')} " f"({_fmt_percent(market_snapshot.get('top_bucket_prob'))})" ) + if market_snapshot.get("market_url"): + lines_en.append(f"Market link: {market_snapshot.get('market_url')}") lines_en.append(f"Action: {advice}") lines_en.append(f"Map: {final_map}")