修正市场监控温度数据源:接入 NOAA MADIS 5分钟高频数据并优化展示
- 首尔/釜山:隐藏大号跑道温度值,改为"跑道温度"标签(跑道表面温度 ≠ 空气温度) - US 城市:MADIS HFMETAR 5分钟小数温度接入 airport_primary,前端优先读取 - 其他城市:整数值不再强制 toFixed(1) 追加虚假 .0 精度 - 后端:weather_sources.py 注入 madis_hfmetar_current 到 results - 后端:country_networks.py 的 _airport_primary_from_raw 新增 MADIS 优先分支 - 文档:更新 AIRPORT_REALTIME_SOURCES.md 新增 11 个 US 城市 - 文档:更新 CLAUDE.md 补充市场监控、高频数据管道、Country Network Provider 架构 Tested: npx tsc --noEmit ✓ ruff check . ✓
This commit is contained in:
@@ -44,6 +44,9 @@ Users (Web / Telegram) → Next.js Frontend (Vercel) → FastAPI /web/app.py
|
||||
- **Shared analysis core** in `src/` is used by both web API and bot
|
||||
- **Scan Terminal**: Real-time city opportunity scanning (`web/scan_terminal_service.py` and `frontend/components/dashboard/scan-terminal/`)
|
||||
- **Dashboard**: Main dashboard with interactive map, city sidebar, detail panels, and probability views
|
||||
- **Market Monitor** (`MonitorPanel`): Real-time temperature monitoring board for 22 trading cities. Uses a temperature resolution chain (AMOS runway → AMOS → `airport_primary` → `airport_current` → `current`) defined in `frontend/components/dashboard/monitoring/monitor-temperature.ts`. Per-city refresh decisions driven by source-aware freshness (`source-freshness.ts`) instead of uniform `obs_age_min`. Seoul/Busan display runway surface temperature from AMOS; US cities get 5-min MADIS HFMETAR via `airport_primary`; others fall back to METAR.
|
||||
- **High-Freq Airport Pipeline**: 19 of 22 monitor cities have dedicated realtime sources (AMOS, MADIS, JMA, MGM, FMI, KNMI, AROME). Data flows: `weather_sources.py` (fetch) → `country_networks.py` (`_airport_primary_from_raw`, per-country providers) → API `airport_primary` field. Plain METAR stays in `airport_current`. Documented in `docs/AIRPORT_REALTIME_SOURCES.md`.
|
||||
- **Country Network Providers**: `country_networks.py` routes per-city to the right provider (Turkey→MGM, Korea→KMA, Japan→JMA, etc.) via `get_country_network_provider()`. Each provider controls `airport_primary_current`, `official_nearby_current`, and `official_network_status`. US cities use the default `GlobalMetarNetworkProvider` but get MADIS overrides injected via `results["madis_hfmetar_current"]`.
|
||||
|
||||
## Commands
|
||||
|
||||
@@ -98,7 +101,7 @@ curl http://127.0.0.1:8000/metrics
|
||||
|
||||
| Directory | Purpose |
|
||||
|-----------|---------|
|
||||
| `src/data_collection/` | Weather sources (METAR, TAF, Open-Meteo, JMA, KMA, MGM, NMC, Russia stations, settlement sources), city registry (52 cities), Polymarket readonly layer |
|
||||
| `src/data_collection/` | Weather sources (METAR, TAF, Open-Meteo, JMA, KMA, MGM, NMC, Russia stations, settlement sources), city registry (52 cities), Polymarket readonly layer. Also: `madis_sources.py` (NOAA 5-min NetCDF), `amos_station_sources.py` (Korean runway sensors), `country_networks.py` (per-country provider routing + `_airport_primary_from_raw`) |
|
||||
| `src/analysis/` | DEB algorithm, trend engine, probability calibration (EMOS/LGBM), market alert engine, settlement rounding |
|
||||
| `src/models/` | LightGBM daily-high model training and feature engineering |
|
||||
| `src/payments/` | Onchain checkout, event listener, confirm loop, contract audit |
|
||||
@@ -106,8 +109,8 @@ curl http://127.0.0.1:8000/metrics
|
||||
| `src/database/` | SQLite-based runtime state, DB manager, daily/truth/training feature repositories |
|
||||
| `web/` | FastAPI app, routes (~65K), analysis service (~130K), scan terminal service (~56K), AI scan modules |
|
||||
| `frontend/app/` | Next.js App Router pages (dashboard, account, auth, docs, ops, probabilities, scan) |
|
||||
| `frontend/components/dashboard/` | Dashboard UI components (map, sidebar, detail panel, modals, charts, scan terminal). `scan-root-styles.ts` is the CSS Module barrel, combining 22 module roots into one pre-composed className |
|
||||
| `frontend/lib/` | Shared client logic: types, API client, chart utils, i18n, dashboard utils |
|
||||
| `frontend/components/dashboard/` | Dashboard UI components (map, sidebar, detail panel, modals, charts, scan terminal). `scan-root-styles.ts` is the CSS Module barrel, combining 22 module roots into one pre-composed className. `monitoring/` subdirectory: `MonitorPanel`, `monitor-temperature.ts` (temp resolution chain), `monitor-refresh-policy.ts`. |
|
||||
| `frontend/lib/` | Shared client logic: types (`dashboard-types.ts`, including `AirportCurrentConditions`, `CityDetail`), API client, chart utils, i18n, `source-freshness.ts` (per-source freshness with `expected_next_update_at`), dashboard utils |
|
||||
| `frontend/hooks/` | React hooks: dashboard store (global state), Leaflet map, chart helper |
|
||||
| `scripts/` | Operational scripts: probability calibration training, backfills, payment reconciliation, prewarm worker |
|
||||
| `config/` | YAML config (city list, weather settings, logging) |
|
||||
@@ -138,6 +141,7 @@ This repo uses the **Lore Commit Protocol** — structured decision records with
|
||||
- When modifying UI components, update both **dark-mode and light-mode CSS files** in the same edit batch.
|
||||
- **CSS Variables First**: Prefer `var(--color-*)` / `var(--color-signal-*)` tokens over hardcoded hex values. The token system is defined in `globals.css` with light-theme overrides under `html.light`.
|
||||
- **Avoid `!important`**: Only use it for Leaflet map overrides (inline style conflict) and chart canvas sizing. For light-theme overrides, use `html.light .root` prefix for higher specificity.
|
||||
- **Monitoring CSS note**: `MonitorPanel.module.css` scopes its light-theme overrides to `.scan-terminal.light` (the terminal's built-in toggle), NOT `html.light`. When adding light styles for monitoring components, match this scoping.
|
||||
- **New CSS Modules**: Add the module root class to `scan-root-styles.ts` barrel file instead of importing it separately in `ScanTerminalDashboard.tsx`.
|
||||
|
||||
## Quality Gates (MANDATORY)
|
||||
@@ -146,7 +150,7 @@ Before marking any task as complete, you MUST:
|
||||
|
||||
1. **Type check** — Run `npx tsc --noEmit` (frontend) or `python -m ruff check .` (backend) on modified files
|
||||
2. **No Unicode escapes** — Verify that NO `\uXXXX` sequences were introduced; if found, revert and fix
|
||||
3. **Dual-theme CSS** — For any UI change, confirm BOTH the dark CSS module AND `ScanTerminalLightTheme.module.css` were updated
|
||||
3. **Dual-theme CSS** — For any UI change, confirm BOTH dark and light styles. Most components need `ScanTerminalLightTheme.module.css` updated; monitoring components (`MonitorPanel.module.css`) contain their own `.scan-terminal.light` blocks inline.
|
||||
4. **No new hardcoded palette colors** — Use `var(--color-*)` token references instead of `#4DA3FF` / `#E6EDF3` / `#9FB2C7` / `#6B7A90` hex values
|
||||
5. **Show the diff** — Output `git diff --stat` and test results before declaring success
|
||||
|
||||
|
||||
@@ -12,6 +12,23 @@
|
||||
| 赫尔辛基 | Vantaa | EFHK | FMI (`opendata.fmi.fi`) | 10 分钟 | 机场站点实时温度 | 免费 |
|
||||
| 阿姆斯特丹 | Schiphol | EHAM | KNMI (`dataplatform.knmi.nl`) | 10 分钟 | 机场站点实时温度 | 免费(需注册) |
|
||||
| 巴黎 | Le Bourget | LFPB | AROME HD (`api.open-meteo.com`) | 15 分钟 | 模型预报(非实测) | 免费 |
|
||||
| 纽约 | LaGuardia | KLGA | NOAA MADIS HFMETAR | 5 分钟 | 机场站点实时温度 | 免费 |
|
||||
| 洛杉矶 | LAX | KLAX | NOAA MADIS HFMETAR | 5 分钟 | 机场站点实时温度 | 免费 |
|
||||
| 芝加哥 | O'Hare | KORD | NOAA MADIS HFMETAR | 5 分钟 | 机场站点实时温度 | 免费 |
|
||||
| 丹佛 | Buckley | KBKF | NOAA MADIS HFMETAR | 5 分钟 | 机场站点实时温度 | 免费 |
|
||||
| 亚特兰大 | Hartsfield | KATL | NOAA MADIS HFMETAR | 5 分钟 | 机场站点实时温度 | 免费 |
|
||||
| 迈阿密 | MIA | KMIA | NOAA MADIS HFMETAR | 5 分钟 | 机场站点实时温度 | 免费 |
|
||||
| 旧金山 | SFO | KSFO | NOAA MADIS HFMETAR | 5 分钟 | 机场站点实时温度 | 免费 |
|
||||
| 休斯顿 | Hobby | KHOU | NOAA MADIS HFMETAR | 5 分钟 | 机场站点实时温度 | 免费 |
|
||||
| 达拉斯 | Love Field | KDAL | NOAA MADIS HFMETAR | 5 分钟 | 机场站点实时温度 | 免费 |
|
||||
| 奥斯汀 | Bergstrom | KAUS | NOAA MADIS HFMETAR | 5 分钟 | 机场站点实时温度 | 免费 |
|
||||
| 西雅图 | SeaTac | KSEA | NOAA MADIS HFMETAR | 5 分钟 | 机场站点实时温度 | 免费 |
|
||||
|
||||
> **NOAA MADIS HFMETAR**: 美国 11 个城市的机场高频实时数据通过 NOAA MADIS 公共档案获取。
|
||||
> 数据源为 NetCDF 格式(`madis-data.ncep.noaa.gov/madisPublic1/data/LDAD/hfmetar/`),
|
||||
> 每 5 分钟全量更新一次,温度保留一位小数。匿名公开访问,无需 API 密钥。
|
||||
> 后端通过 `weather_sources.py` 拉取并注入 `airport_primary`,前端市场监控通过
|
||||
> `resolveMonitorTemperature` 优先读取 `airport_primary.temp` 获得小数精度温度。
|
||||
|
||||
## 推送机制
|
||||
|
||||
|
||||
@@ -215,6 +215,13 @@
|
||||
color: var(--color-border-default);
|
||||
}
|
||||
|
||||
.root :global(.monitor-temp-runway) {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-muted);
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
/* ── Stats row ── */
|
||||
.root :global(.monitor-stats) {
|
||||
font-size: 14px;
|
||||
@@ -421,6 +428,7 @@
|
||||
|
||||
.root :global(.scan-terminal.light .monitor-temp-unit),
|
||||
.root :global(.scan-terminal.light .monitor-temp-missing),
|
||||
.root :global(.scan-terminal.light .monitor-temp-runway),
|
||||
.root :global(.scan-terminal.light .monitor-stat-missing) {
|
||||
color: #94A3B8;
|
||||
}
|
||||
@@ -553,6 +561,7 @@
|
||||
|
||||
.root :global(.scan-terminal.light .monitor-temp-unit),
|
||||
.root :global(.scan-terminal.light .monitor-temp-missing),
|
||||
.root :global(.scan-terminal.light .monitor-temp-runway),
|
||||
.root :global(.scan-terminal.light .monitor-stat-missing) {
|
||||
color: #94A3B8;
|
||||
}
|
||||
|
||||
@@ -476,7 +476,10 @@ export default function MonitorPanel({
|
||||
}
|
||||
|
||||
const ac = detail.airport_current;
|
||||
const cur = resolveMonitorTemperature(detail).value;
|
||||
const tempInfo = resolveMonitorTemperature(detail);
|
||||
const cur = tempInfo.value;
|
||||
const curSource = tempInfo.source;
|
||||
const isRunwayTemp = curSource === "amos_runway_median" || curSource === "amos_runway";
|
||||
const max = resolveMaxSoFar(detail, key); // HKO cities fall back to current.max_so_far
|
||||
const mtt = ac?.max_temp_time ?? detail.current?.max_temp_time ?? null;
|
||||
const freshnessInfo = getObservationFreshness(detail);
|
||||
@@ -533,12 +536,16 @@ export default function MonitorPanel({
|
||||
<span className="monitor-obs-time">{obs}</span>
|
||||
</div>
|
||||
|
||||
{/* Temperature */}
|
||||
{/* Temperature — runway cities skip the large value (runway surface ≠ air temp) */}
|
||||
<div className="monitor-temp-display">
|
||||
{cur != null ? (
|
||||
{isRunwayTemp ? (
|
||||
<span className="monitor-temp-runway">
|
||||
{t("Runway Temp", "跑道温度", lang)}
|
||||
</span>
|
||||
) : cur != null ? (
|
||||
<>
|
||||
<span className={`monitor-temp-value${newHigh ? " new-high" : warm ? " warm" : ""}${isFlashing ? " flashed" : ""}`}>
|
||||
{cur.toFixed(1)}
|
||||
{Number.isInteger(cur) ? cur.toFixed(0) : cur.toFixed(1)}
|
||||
</span>
|
||||
<span className="monitor-temp-unit">{tempSymbol}</span>
|
||||
</>
|
||||
@@ -553,7 +560,7 @@ export default function MonitorPanel({
|
||||
<span className="monitor-stat-label">{t("Today's High", "今日实测高温", lang)}</span>
|
||||
{max != null ? (
|
||||
<>
|
||||
<span className="monitor-high-value">{max.toFixed(1)}{tempSymbol}</span>
|
||||
<span className="monitor-high-value">{Number.isInteger(max) ? max.toFixed(0) : max.toFixed(1)}{tempSymbol}</span>
|
||||
{mtt && <span className="monitor-high-time">{mtt}</span>}
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -4,6 +4,7 @@ export type MonitorTemperatureSource =
|
||||
| "amos_runway_median"
|
||||
| "amos_runway"
|
||||
| "amos"
|
||||
| "airport_primary"
|
||||
| "airport_current"
|
||||
| "current"
|
||||
| "missing";
|
||||
@@ -54,6 +55,9 @@ export function resolveMonitorTemperature(
|
||||
return { source: "amos", value: amosTemp };
|
||||
}
|
||||
|
||||
const airportPrimary = finiteNumber(detail?.airport_primary?.temp);
|
||||
if (airportPrimary != null) return { source: "airport_primary", value: airportPrimary };
|
||||
|
||||
const airport = finiteNumber(detail?.airport_current?.temp);
|
||||
if (airport != null) return { source: "airport_current", value: airport };
|
||||
|
||||
|
||||
@@ -242,6 +242,37 @@ def _airport_primary_from_raw(city: str, raw: Dict[str, Any]) -> Dict[str, Any]:
|
||||
meta = _city_meta(city)
|
||||
metar = raw.get("metar") or {}
|
||||
current = metar.get("current") or {}
|
||||
|
||||
# High-frequency realtime sources take priority over plain METAR.
|
||||
madis = raw.get("madis_hfmetar_current") or {}
|
||||
if madis.get("temp_c") is not None:
|
||||
return _normalize_station_row(
|
||||
station_code=meta.get("icao") or madis.get("icao"),
|
||||
station_label=meta.get("airport_name") or meta.get("icao"),
|
||||
temp=madis["temp_c"],
|
||||
obs_time=madis.get("obs_time") or metar.get("observation_time"),
|
||||
source_code="madis_hfmetar",
|
||||
source_label="NOAA MADIS",
|
||||
is_official=True,
|
||||
is_airport_station=True,
|
||||
is_settlement_anchor=False,
|
||||
extra={
|
||||
"max_so_far": _safe_float(current.get("max_temp_so_far")),
|
||||
"max_temp_time": current.get("max_temp_time"),
|
||||
"obs_age_min": None,
|
||||
"report_time": metar.get("report_time"),
|
||||
"receipt_time": metar.get("receipt_time"),
|
||||
"obs_time_epoch": metar.get("obs_time_epoch"),
|
||||
"obs_time_utc_offset_seconds": 0,
|
||||
"wind_speed_kt": _safe_float(madis.get("wind_kt")) or _safe_float(current.get("wind_speed_kt")),
|
||||
"wind_dir": _safe_float(current.get("wind_dir")),
|
||||
"humidity": _safe_float(current.get("humidity")),
|
||||
"visibility_mi": _safe_float(current.get("visibility_mi")),
|
||||
"wx_desc": current.get("wx_desc"),
|
||||
"raw_metar": current.get("raw_metar"),
|
||||
},
|
||||
)
|
||||
|
||||
return _normalize_station_row(
|
||||
station_code=meta.get("icao") or metar.get("icao"),
|
||||
station_label=meta.get("airport_name") or metar.get("station_name") or metar.get("icao"),
|
||||
|
||||
@@ -1084,6 +1084,8 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour
|
||||
# Find this city's station in the MADIS results
|
||||
for obs in obs_list:
|
||||
if obs["icao"] == icao:
|
||||
# Inject into results so it flows through to airport_primary
|
||||
results["madis_hfmetar_current"] = obs
|
||||
try:
|
||||
DBManager().append_airport_obs(
|
||||
icao=icao,
|
||||
|
||||
Reference in New Issue
Block a user