feat: Introduce dashboard data types, utilities, market alert engine, and panel sections.
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user