From 96676e7097e7e504c3decf5c10b54615c2dd88aa Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Thu, 14 May 2026 16:00:55 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E5=B8=82=E5=9C=BA=E7=9B=91?= =?UTF-8?q?=E6=8E=A7=E6=B8=A9=E5=BA=A6=E6=95=B0=E6=8D=AE=E6=BA=90=EF=BC=9A?= =?UTF-8?q?=E6=8E=A5=E5=85=A5=20NOAA=20MADIS=205=E5=88=86=E9=92=9F?= =?UTF-8?q?=E9=AB=98=E9=A2=91=E6=95=B0=E6=8D=AE=E5=B9=B6=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 首尔/釜山:隐藏大号跑道温度值,改为"跑道温度"标签(跑道表面温度 ≠ 空气温度) - 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 . ✓ --- CLAUDE.md | 12 ++++--- docs/AIRPORT_REALTIME_SOURCES.md | 17 ++++++++++ .../monitoring/MonitorPanel.module.css | 9 ++++++ .../dashboard/monitoring/MonitorPanel.tsx | 17 +++++++--- .../monitoring/monitor-temperature.ts | 4 +++ src/data_collection/country_networks.py | 31 +++++++++++++++++++ src/data_collection/weather_sources.py | 2 ++ 7 files changed, 83 insertions(+), 9 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 6cc63a62..671034a8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/docs/AIRPORT_REALTIME_SOURCES.md b/docs/AIRPORT_REALTIME_SOURCES.md index b21a6340..b23c7c72 100644 --- a/docs/AIRPORT_REALTIME_SOURCES.md +++ b/docs/AIRPORT_REALTIME_SOURCES.md @@ -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` 获得小数精度温度。 ## 推送机制 diff --git a/frontend/components/dashboard/monitoring/MonitorPanel.module.css b/frontend/components/dashboard/monitoring/MonitorPanel.module.css index 888c6d15..ae5b5d81 100644 --- a/frontend/components/dashboard/monitoring/MonitorPanel.module.css +++ b/frontend/components/dashboard/monitoring/MonitorPanel.module.css @@ -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; } diff --git a/frontend/components/dashboard/monitoring/MonitorPanel.tsx b/frontend/components/dashboard/monitoring/MonitorPanel.tsx index d38935b5..22996051 100644 --- a/frontend/components/dashboard/monitoring/MonitorPanel.tsx +++ b/frontend/components/dashboard/monitoring/MonitorPanel.tsx @@ -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({ {obs} - {/* Temperature */} + {/* Temperature — runway cities skip the large value (runway surface ≠ air temp) */}
- {cur != null ? ( + {isRunwayTemp ? ( + + {t("Runway Temp", "跑道温度", lang)} + + ) : cur != null ? ( <> - {cur.toFixed(1)} + {Number.isInteger(cur) ? cur.toFixed(0) : cur.toFixed(1)} {tempSymbol} @@ -553,7 +560,7 @@ export default function MonitorPanel({ {t("Today's High", "今日实测高温", lang)} {max != null ? ( <> - {max.toFixed(1)}{tempSymbol} + {Number.isInteger(max) ? max.toFixed(0) : max.toFixed(1)}{tempSymbol} {mtt && {mtt}} ) : ( diff --git a/frontend/components/dashboard/monitoring/monitor-temperature.ts b/frontend/components/dashboard/monitoring/monitor-temperature.ts index 80ed26f1..25fc2001 100644 --- a/frontend/components/dashboard/monitoring/monitor-temperature.ts +++ b/frontend/components/dashboard/monitoring/monitor-temperature.ts @@ -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 }; diff --git a/src/data_collection/country_networks.py b/src/data_collection/country_networks.py index ceaefcfa..9ffc4fc4 100644 --- a/src/data_collection/country_networks.py +++ b/src/data_collection/country_networks.py @@ -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"), diff --git a/src/data_collection/weather_sources.py b/src/data_collection/weather_sources.py index 6b5f8751..9757042d 100644 --- a/src/data_collection/weather_sources.py +++ b/src/data_collection/weather_sources.py @@ -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,