feat: Introduce dashboard data types, utilities, market alert engine, and panel sections.

This commit is contained in:
2569718930@qq.com
2026-03-10 10:07:18 +08:00
parent 732b3c10a5
commit e5011c5c8f
4 changed files with 83 additions and 9 deletions
@@ -550,7 +550,7 @@ export function ModelForecast({
hideTitle?: boolean; hideTitle?: boolean;
targetDate?: string | null; targetDate?: string | null;
}) { }) {
const { t } = useI18n(); const { locale, t } = useI18n();
const view = getModelView(detail, targetDate); const view = getModelView(detail, targetDate);
const modelEntries = Object.entries(view.models).filter(([, value]) => const modelEntries = Object.entries(view.models).filter(([, value]) =>
Number.isFinite(Number(value)), Number.isFinite(Number(value)),
@@ -565,6 +565,14 @@ export function ModelForecast({
? Math.max(...comparisonValues) + 1 ? Math.max(...comparisonValues) + 1
: 1; : 1;
const range = Math.max(maxValue - minValue, 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 ( return (
<section className="models-section"> <section className="models-section">
@@ -587,7 +595,7 @@ export function ModelForecast({
return ( return (
<div key={name} className="model-row"> <div key={name} className="model-row">
<div className="model-name" title={name}> <div className="model-name" title={name}>
{name} {getModelDisplayName(name)}
</div> </div>
<div className="model-bar-track"> <div className="model-bar-track">
<div <div
+3
View File
@@ -169,7 +169,10 @@ export interface SourceForecasts {
forecast_periods?: WeatherGovPeriod[]; forecast_periods?: WeatherGovPeriod[];
}; };
meteoblue?: { meteoblue?: {
today_high?: number | null;
daily_highs?: Array<number | null>; daily_highs?: Array<number | null>;
unit?: string | null;
url?: string | null;
}; };
} }
+42 -2
View File
@@ -330,18 +330,58 @@ export function getProbabilityView(detail: CityDetail, targetDate?: string | nul
} }
export function getModelView(detail: CityDetail, targetDate?: string | null) { 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<string, number | null>,
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 date = targetDate || detail.local_date;
const daily = detail.multi_model_daily?.[date]; const daily = detail.multi_model_daily?.[date];
if (daily) { if (daily) {
return { return {
deb: daily.deb?.prediction ?? null, deb: daily.deb?.prediction ?? null,
models: daily.models || {}, models: withMeteoblue(daily.models || {}, date),
}; };
} }
return { return {
deb: detail.deb?.prediction ?? null, deb: detail.deb?.prediction ?? null,
models: detail.multi_model || {}, models: withMeteoblue(detail.multi_model || {}, date),
}; };
} }
+28 -5
View File
@@ -5,6 +5,7 @@ Rule-based weather alert engine for short-horizon trading signals.
from __future__ import annotations from __future__ import annotations
import math import math
import re
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Any, Dict, List, Optional 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() or str(bucket.get("range") or "").strip()
) )
if direct: if direct:
return direct normalized = re.sub(
r"(?<!°)(-?\d+(?:\.\d+)?)\s*C(\+)?",
r"\1°C\2",
direct,
flags=re.IGNORECASE,
)
return normalized
value = _sf(bucket.get("value")) value = _sf(bucket.get("value"))
if value is not None: if value is not None:
return f"{round(value)}C" return f"{round(value)}°C"
temp = _sf(bucket.get("temp")) temp = _sf(bucket.get("temp"))
if temp is not None: if temp is not None:
return f"{round(temp)}C" return f"{round(temp)}°C"
return None return None
@@ -507,6 +514,17 @@ def _extract_market_snapshot(city_weather: Dict[str, Any]) -> 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 { return {
"available": True, "available": True,
"selected_bucket": _bucket_label(scan.get("temperature_bucket")), "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"), "signal_label": scan.get("signal_label"),
"confidence": scan.get("confidence"), "confidence": scan.get("confidence"),
"top_bucket_rows": top_bucket_rows, "top_bucket_rows": top_bucket_rows,
"market_url": market_url,
} }
@@ -672,7 +691,7 @@ def _build_telegram_messages(
label = row.get("label") or "--" label = row.get("label") or "--"
prob_text = _fmt_percent(row.get("probability")) prob_text = _fmt_percent(row.get("probability"))
yes_buy_text = _fmt_cents(row.get("yes_buy")) 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"): if market_snapshot.get("available") and not market_snapshot.get("top_bucket_rows"):
market_edge = _sf(market_snapshot.get("edge_percent")) market_edge = _sf(market_snapshot.get("edge_percent"))
market_edge_text = f"{market_edge:+.1f}%" if market_edge is not None else "--" 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"市场最热桶:{market_snapshot.get('top_bucket')} "
f"({_fmt_percent(market_snapshot.get('top_bucket_prob'))})" 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"AI 建议:{advice}")
lines_zh.append(f"点击查看实时地图:{final_map}") lines_zh.append(f"点击查看实时地图:{final_map}")
@@ -739,7 +760,7 @@ def _build_telegram_messages(
label = row.get("label") or "--" label = row.get("label") or "--"
prob_text = _fmt_percent(row.get("probability")) prob_text = _fmt_percent(row.get("probability"))
yes_buy_text = _fmt_cents(row.get("yes_buy")) 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"): if market_snapshot.get("available") and not market_snapshot.get("top_bucket_rows"):
market_edge = _sf(market_snapshot.get("edge_percent")) market_edge = _sf(market_snapshot.get("edge_percent"))
market_edge_text = f"{market_edge:+.1f}%" if market_edge is not None else "--" 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"Top market bucket: {market_snapshot.get('top_bucket')} "
f"({_fmt_percent(market_snapshot.get('top_bucket_prob'))})" 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"Action: {advice}")
lines_en.append(f"Map: {final_map}") lines_en.append(f"Map: {final_map}")