feat: introduce PolyWeather web map API and frontend with integrated weather data collection and analysis.
This commit is contained in:
@@ -41,11 +41,11 @@ This project utilizes a production-grade decoupled architecture for high availab
|
||||
## 🚀 Core Features
|
||||
|
||||
- **📡 Full-Spectrum Collection**
|
||||
- **Major Models**: Real-time sync for ECMWF, GFS, ICON, GEM, and JMA high temperatures.
|
||||
- **Observed Data**: Global airport METAR reports + official Turkish MGM station-level data.
|
||||
- **Centralized Correction**: Integrated `17130` (Center) official data specifically for Ankara.
|
||||
- **Major Models**: Real-time sync for ECMWF, GFS, ICON, GEM, JMA, and Open-Meteo daily/hourly guidance.
|
||||
- **Observed Data**: Aviation Weather / METAR as the primary observation source, plus official Turkish MGM station-level data for Ankara.
|
||||
- **City Specialization**: Integrated `17130` (`Ankara (Bölge/Center)`) as the key Ankara lead station, without replacing LTAC settlement observation.
|
||||
- **⚖️ DEB Smart Blending**
|
||||
- Dynamic weighting of forecasts based on recent 7-day historical performance.
|
||||
- Dynamic weighting of forecasts based on recent historical performance and city-specific reliability.
|
||||
- **🔔 Alert Engine**
|
||||
- **Momentum Spike**: Captures rapid temperature changes within 30 minutes.
|
||||
- **Forecast Breakthrough**: Fires when observations exceed all model predictions plus a safety margin.
|
||||
@@ -67,6 +67,23 @@ This project utilizes a production-grade decoupled architecture for high availab
|
||||
|
||||
---
|
||||
|
||||
## 🧭 Current Data Logic
|
||||
|
||||
- **Primary observation source**: Aviation Weather / METAR
|
||||
- **Ankara enhancement**:
|
||||
- Settlement observation: `LTAC / Esenboğa`
|
||||
- Official lead station: `Ankara (Bölge/Center)` / `17130`
|
||||
- Nearby station layer: Turkish MGM station network
|
||||
- **Other cities nearby layer**:
|
||||
- Current production logic uses Aviation Weather METAR clusters
|
||||
- U.S. cities may later receive Mesonet augmentation, but METAR remains the base layer
|
||||
- **Historical benchmarking rules**:
|
||||
- Web dashboard: rolling last 15 settled days
|
||||
- Telegram `/deb`: rolling last 7 settled days
|
||||
- The current in-progress day is excluded from win rate and MAE
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ System Architecture
|
||||
|
||||
```mermaid
|
||||
@@ -127,7 +144,7 @@ Associate the `frontend` directory as the project root on Vercel for automatic C
|
||||
| :-------- | :-------------------------------------- | :------------- |
|
||||
| `/city` | Query real-time analysis for a city | `/city ankara` |
|
||||
| `/deb` | View historical accuracy of DEB model | `/deb london` |
|
||||
| `/points` | View your activity points & leaderboard | `/points` |
|
||||
| `/top` | View activity leaderboard | `/top` |
|
||||
| `/help` | Get detailed instructions | `/help` |
|
||||
|
||||
---
|
||||
@@ -136,11 +153,14 @@ Associate the `frontend` directory as the project root on Vercel for automatic C
|
||||
> **Commercialization**: This project currently offers **Web Dashboard ($5/mo)** and **Telegram Signal Channel ($1/mo)** subscriptions.
|
||||
> Point-earning via group participation is active and points can be redeemed for access.
|
||||
|
||||
> [!NOTE]
|
||||
> **Frontend Model**: The production website still uses the legacy high-density dashboard shell under `frontend/public/legacy/index.html`, embedded through the Next.js app shell.
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
**📅 Last Updated**: 2026-03-08
|
||||
**📅 Last Updated**: 2026-03-09
|
||||
**🚀 Status**: v1.0 Stable - Professional Quant UI Locked
|
||||
|
||||
> [!TIP]
|
||||
|
||||
+26
-6
@@ -41,11 +41,11 @@ PolyWeather 是一套专为 **Polymarket** 深度博弈者设计的实时情报
|
||||
## 🚀 核心功能
|
||||
|
||||
- **📡 多源全量采集**
|
||||
- **主流模型**:ECMWF, GFS, ICON, GEM, JMA 实时最高温同步。
|
||||
- **实测数据**:全球机场 METAR 定时报文 + 土耳其 MGM 局点官方实测。
|
||||
- **中心化纠偏**:针对安卡拉特别接入 `17130` (Center) 官方指挥中心数据。
|
||||
- **主流模型**:ECMWF、GFS、ICON、GEM、JMA、Open-Meteo 的日/小时温度指导。
|
||||
- **实测数据**:Aviation Weather / METAR 作为主观测源,安卡拉叠加土耳其 MGM 官方站点实测。
|
||||
- **城市特化**:安卡拉特别接入 `17130`(`Ankara (Bölge/Center)`)作为领先站,不替代 LTAC 结算主站。
|
||||
- **⚖️ DEB 智能融合**
|
||||
- 基于近期 7 天历史表现,动态调整各模型权重的博弈预测。
|
||||
- 基于近期历史表现和城市级可靠性,动态调整各模型权重。
|
||||
- **🔔 异动预警系统 (Alert Engine)**
|
||||
- **动量突变**:捕捉 30 分钟内的急剧温变。
|
||||
- **预测突破**:当实测击穿所有预报上限时触发告警。
|
||||
@@ -67,6 +67,23 @@ PolyWeather 是一套专为 **Polymarket** 深度博弈者设计的实时情报
|
||||
|
||||
---
|
||||
|
||||
## 🧭 当前数据逻辑
|
||||
|
||||
- **主观测源**:Aviation Weather / METAR
|
||||
- **安卡拉增强逻辑**:
|
||||
- 结算主观测:`LTAC / Esenboğa`
|
||||
- 官方领先站:`Ankara (Bölge/Center)` / `17130`
|
||||
- 周边站层:土耳其 MGM 周边站网络
|
||||
- **其他城市周边站层**:
|
||||
- 生产环境当前使用 Aviation Weather METAR cluster
|
||||
- 美国城市后续可追加 Mesonet,但 METAR 仍是基础层
|
||||
- **历史对账口径**:
|
||||
- 网页端:滚动近 15 个已结算自然日
|
||||
- Telegram `/deb`:滚动近 7 个已结算自然日
|
||||
- 当天未结算样本不参与胜率与 MAE 统计
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 架构解析
|
||||
|
||||
```mermaid
|
||||
@@ -127,7 +144,7 @@ docker-compose up -d --build
|
||||
| :-------- | :------------------------ | :------------- |
|
||||
| `/city` | 查询指定城市实时分析 | `/city ankara` |
|
||||
| `/deb` | 查看 DEB 模型的历史准确率 | `/deb london` |
|
||||
| `/points` | 查看您的活跃积分与排行榜 | `/points` |
|
||||
| `/top` | 查看活跃积分排行榜 | `/top` |
|
||||
| `/help` | 获取详细功能说明 | `/help` |
|
||||
|
||||
---
|
||||
@@ -136,11 +153,14 @@ docker-compose up -d --build
|
||||
> **商业化提示**:本项目目前提供 **Web 仪表盘 ($5/月)** 与 **Telegram 信号频道 ($1/月)** 订阅服务。
|
||||
> 发言获取积分逻辑已上线,活跃用户可兑换相应权限。
|
||||
|
||||
> [!NOTE]
|
||||
> **前端现状**:生产环境网页仍使用 `frontend/public/legacy/index.html` 的高密度 legacy dashboard,由 Next.js 外壳嵌入并在 Vercel 上部署。
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
**📅 最后更新**:2026-03-08
|
||||
**📅 最后更新**:2026-03-09
|
||||
**🚀 状态**:v1.0 稳定版 - 专业量化 UI 已锁定
|
||||
|
||||
> [!TIP]
|
||||
|
||||
+162
-84
@@ -1,26 +1,32 @@
|
||||
# PolyWeather API 接口文档 (v1.0)
|
||||
# PolyWeather API 接口文档 (v1.1)
|
||||
|
||||
本文档详细介绍了 PolyWeather 后端提供的 RESTful API 接口。该后端基于 FastAPI 构建,主要为前端地图和终端面板提供实时天气分析、模型预测及历史对账数据。
|
||||
本文档说明当前 PolyWeather 后端实际提供的 HTTP API。后端由 `web/app.py` 提供,前端网页通过 Next.js BFF 代理访问这些接口。
|
||||
|
||||
---
|
||||
|
||||
## 1. 基础信息
|
||||
|
||||
- **Base URL**: `http://127.0.0.1:8000` (本地) 或 `https://your-vps-ip:8000` (服务器)
|
||||
- **数据格式**: JSON
|
||||
- **速率限制**: 默认无限制,建议前端缓存时间 5 分钟。
|
||||
- **本地 Base URL**: `http://127.0.0.1:8000`
|
||||
- **生产 Base URL**: `http://<your-vps-ip>:8000` 或绑定后的 HTTPS API 域名
|
||||
- **响应格式**: JSON
|
||||
- **缓存策略**:
|
||||
- `/api/cities`: 5 分钟
|
||||
- `/api/city/{name}`: 30 秒
|
||||
- `/api/history/{name}`: 15 分钟
|
||||
- `/api/city/{name}/summary`: 30 秒
|
||||
- `/api/city/{name}/detail`: 30 秒
|
||||
|
||||
---
|
||||
|
||||
## 2. 接口列表
|
||||
|
||||
### 2.1 获取城市列表
|
||||
|
||||
返回所有受监控的城市及其基础坐标和风险等级。
|
||||
### 2.1 获取监控城市列表
|
||||
|
||||
- **URL**: `/api/cities`
|
||||
- **Method**: `GET`
|
||||
- **返回示例**:
|
||||
- **用途**: 返回首页左侧监控城市与世界地图 marker 的基础元数据。
|
||||
|
||||
**响应示例**
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -28,11 +34,11 @@
|
||||
{
|
||||
"name": "ankara",
|
||||
"display_name": "Ankara",
|
||||
"lat": 39.93,
|
||||
"lon": 32.85,
|
||||
"lat": 40.1281,
|
||||
"lon": 32.9951,
|
||||
"risk_level": "medium",
|
||||
"risk_emoji": "🟡",
|
||||
"airport": "Esenboga",
|
||||
"airport": "Esenboğa",
|
||||
"icao": "LTAC",
|
||||
"temp_unit": "celsius",
|
||||
"is_major": true
|
||||
@@ -41,115 +47,187 @@
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 获取城市详情分析
|
||||
|
||||
获取指定城市的实时实测数据、多模型预测对比、DEB 算法预测以及 AI 决策建议。
|
||||
### 2.2 获取城市实时分析
|
||||
|
||||
- **URL**: `/api/city/{name}`
|
||||
- **Method**: `GET`
|
||||
- **参数**:
|
||||
- `name` (string): 城市名称或别名(不区分大小写,如 `ankara`, `ldn`)。
|
||||
- `force_refresh` (bool, optional): 是否强制跳过缓存刷新数据。默认 `false`。
|
||||
- **返回示例**:
|
||||
- `name`: 城市名或别名,如 `ankara`、`new-york`
|
||||
- `force_refresh` (可选): `true` 时跳过缓存
|
||||
- **用途**: 首页右侧详情面板与地图数据的主接口。
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "ankara",
|
||||
"display_name": "Ankara",
|
||||
"lat": 39.93,
|
||||
"lon": 32.85,
|
||||
"temp_symbol": "°C",
|
||||
"local_time": "2024-03-08 10:30:00",
|
||||
"risk": {
|
||||
"level": "medium",
|
||||
"warning": "模型分歧较大",
|
||||
"icao": "LTAC"
|
||||
},
|
||||
"current": {
|
||||
"temp": 12.5,
|
||||
"max_so_far": 14.2,
|
||||
"obs_time": "10:20",
|
||||
"wx_desc": "Cloudy"
|
||||
},
|
||||
"multi_model": {
|
||||
"ECMWF": 15.1,
|
||||
"GFS": 14.8,
|
||||
"MGM": 15.5
|
||||
},
|
||||
"deb": {
|
||||
"prediction": 15.2
|
||||
},
|
||||
"ai_analysis": "当前模型一致性较好,建议参考 DEB 预测值..."
|
||||
}
|
||||
```
|
||||
**当前核心字段**
|
||||
|
||||
- `display_name`
|
||||
- `local_time`
|
||||
- `local_date`
|
||||
- `temp_symbol`
|
||||
- `risk`
|
||||
- `current`
|
||||
- `mgm`
|
||||
- `mgm_nearby`
|
||||
- `forecast`
|
||||
- `multi_model`
|
||||
- `deb`
|
||||
- `ensemble`
|
||||
- `probabilities`
|
||||
- `trend`
|
||||
- `metar_today_obs`
|
||||
- `metar_recent_obs`
|
||||
- `hourly`
|
||||
- `hourly_next_48h`
|
||||
- `updated_at`
|
||||
|
||||
**说明**
|
||||
|
||||
- `current.raw_metar` 现在直接透出 Aviation Weather API 返回的原始 METAR 报文。
|
||||
- `mgm` 只对 Ankara 这类确实有 Turkish MGM 覆盖的城市有值。
|
||||
- `mgm_nearby` 当前是一个复用字段:
|
||||
- Ankara: Turkish MGM 周边站
|
||||
- 多数其他城市: AviationWeather METAR cluster
|
||||
|
||||
### 2.3 获取历史对账数据
|
||||
|
||||
获取指定城市的过去几天内的预测值与真实最高温的对比记录,用于评估模型准确率。
|
||||
|
||||
- **URL**: `/api/history/{name}`
|
||||
- **Method**: `GET`
|
||||
- **参数**:
|
||||
- `name` (string): 城市名称。
|
||||
- **返回示例**:
|
||||
- **用途**: 历史准确率对账弹窗与机器人 `/deb` 命令的数据基础。
|
||||
|
||||
**响应示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"history": [
|
||||
{
|
||||
"date": "2024-03-07",
|
||||
"actual": 14.0,
|
||||
"deb": 14.2,
|
||||
"mu": 14.1,
|
||||
"mgm": 14.5
|
||||
"date": "2026-03-07",
|
||||
"actual": 7.0,
|
||||
"deb": 6.5,
|
||||
"mu": 7.2,
|
||||
"mgm": 8.0
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**说明**
|
||||
|
||||
- 网页端历史对账只统计 **近 15 天已结算样本**
|
||||
- 当天未结算样本可以画在图里,但不计入胜率与 MAE
|
||||
|
||||
### 2.4 获取城市摘要
|
||||
|
||||
- **URL**: `/api/city/{name}/summary`
|
||||
- **Method**: `GET`
|
||||
- **用途**: 轻量摘要接口,适合未来做 hover 预取或低开销列表更新。
|
||||
|
||||
**字段**
|
||||
|
||||
- `name`
|
||||
- `display_name`
|
||||
- `icao`
|
||||
- `local_time`
|
||||
- `temp_symbol`
|
||||
- `current.temp`
|
||||
- `deb.prediction`
|
||||
- `risk.level`
|
||||
- `risk.warning`
|
||||
- `updated_at`
|
||||
|
||||
### 2.5 获取城市聚合详情
|
||||
|
||||
- **URL**: `/api/city/{name}/detail`
|
||||
- **Method**: `GET`
|
||||
- **用途**: 面向未来的单请求聚合详情接口,便于把 `city + summary + history + future analysis` 整合到一个载荷中。
|
||||
|
||||
**当前结构**
|
||||
|
||||
- `overview`
|
||||
- `official`
|
||||
- `timeseries`
|
||||
- `models`
|
||||
- `probabilities`
|
||||
- `future`
|
||||
|
||||
**说明**
|
||||
|
||||
- 当前首页 legacy 布局还主要使用 `/api/city/{name}` 和 `/api/history/{name}`
|
||||
- `/api/city/{name}/detail` 已用于后续更完整详情态的聚合设计
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心对象定义
|
||||
|
||||
### 3.1 风险等级 (Risk Level)
|
||||
### 3.1 风险等级
|
||||
|
||||
- `low` (🟢): 预测一致,波动小。
|
||||
- `medium` (🟡): 存在轻微模型分歧或数据延迟。
|
||||
- `high` (🔴): 模型严重打架,市场风险极高。
|
||||
- `low`: 低风险,模型与实测较一致
|
||||
- `medium`: 中风险,存在一定分歧或本地站点偏置
|
||||
- `high`: 高风险,模型冲突大或盘面博弈价值高
|
||||
|
||||
### 3.2 预测模型 (Models)
|
||||
### 3.2 DEB
|
||||
|
||||
- `ECMWF`: 欧洲中期天气预报中心(全球最准)。
|
||||
- `GFS`: 美国全球预测系统。
|
||||
- `MGM`: 土耳其国家气象局(针对特定城市非常精准)。
|
||||
- `DEB`: PolyWeather 独家加权融合算法成果。
|
||||
`DEB` 是 PolyWeather 的动态融合预测层,不是简单平均值。它会结合:
|
||||
|
||||
- 多模型预测值
|
||||
- 近期表现
|
||||
- 实况修正
|
||||
- 城市级偏置
|
||||
|
||||
### 3.3 μ
|
||||
|
||||
`μ` 代表当前结算分布中心,是一个**动态期望值**,会随着模型、实况、趋势变化而变化。
|
||||
它不应直接与固定 forecast 用同一口径做静态历史对账。
|
||||
|
||||
---
|
||||
|
||||
## 4. 数据来源与第三方 API
|
||||
## 4. 数据源与第三方 API
|
||||
|
||||
系统通过多源聚合链路确保数据的真实性与前瞻性,以下是目前集成的核心数据源:
|
||||
### 4.1 主观测源
|
||||
|
||||
### 4.1 核心实测数据 (Settlement Data)
|
||||
- **Aviation Weather / METAR**
|
||||
- 当前全球机场主观测源
|
||||
- 同时提供结构化字段与 `rawOb`
|
||||
|
||||
- **NOAA Aviation Weather (METAR)**: 全球机场官方航空气象报文,也是 Polymarket 官方结算参考源。
|
||||
- **Iowa Mesonet (IEM)**: 提供 1-5 分钟级别的超高频 ASOS/METAR 实时观测,是高频交易对账的核心源。
|
||||
- **MGM (土耳其国家气象局)**: 提供土耳其境内(如安卡拉)最精细的本地观测站数据。
|
||||
### 4.2 Ankara 专属源
|
||||
|
||||
### 4.2 模型预测数据 (Forecast Data)
|
||||
- **Turkish MGM**
|
||||
- Ankara 主官方增强层
|
||||
- 包括 `Ankara (Bölge/Center)` 与周边站点
|
||||
|
||||
- **Open-Meteo**: 聚合 ECMWF (欧洲中期)、GFS (美国)、ICON (德国) 等全球顶级大型数值预报模型。
|
||||
- **Meteoblue**: 针对地形复杂的城市提供高精度修正预测(需 API Key)。
|
||||
- **NWS (美国气象局)**: 美国本土城市的官方权威预报。
|
||||
### 4.3 预测源
|
||||
|
||||
### 4.3 智能化分析
|
||||
|
||||
- **Groq API**: 驱动 AI 态势感知引擎,基于多源数据生成实时中文交易分析报告(需 API Key)。
|
||||
- **Open-Meteo**
|
||||
- **weather.gov**(美国城市)
|
||||
- **Meteoblue**(部分城市)
|
||||
- **多模型集合**: ECMWF / GFS / ICON / GEM / JMA
|
||||
|
||||
---
|
||||
|
||||
## 5. 常见问题
|
||||
## 5. 当前口径说明
|
||||
|
||||
- **404 Error**: 检查城市名是否拼写正确,或是否在支持列表中。
|
||||
- **500 Error**: 后端与气象数据源(如 METAR)同步失败,通常 1 分钟后会自动恢复。
|
||||
- **数据延迟**: METAR 实测数据通常每 30-60 分钟更新一次。
|
||||
- 首页地图主 marker 显示 **当前温度**
|
||||
- 右侧详情面板展示当前实测、DEB、结算概率、多模型、多日预报
|
||||
- 未来日期分析模态框:
|
||||
- 显示温度走势、结算概率分布、多模型预报、未来 6-48 小时趋势、未来 0-2 小时临近判断
|
||||
- 已移除独立“冷锋 / 暖锋判断”模块
|
||||
|
||||
---
|
||||
|
||||
## 6. 常见问题
|
||||
|
||||
- **接口 500**
|
||||
- 先检查 `polyweather_web` 是否启动成功
|
||||
- 再看 `docker-compose logs -f polyweather_web`
|
||||
|
||||
- **METAR 看起来慢几分钟**
|
||||
- 通常是官方链路入库/发布延迟,不一定是本地轮询慢
|
||||
- 请同时看:
|
||||
- `current.obs_time`
|
||||
- `current.report_time`
|
||||
- `current.receipt_time`
|
||||
|
||||
- **网页显示旧内容**
|
||||
- 先确认 Vercel 已部署新版本
|
||||
- 再强刷浏览器缓存
|
||||
|
||||
---
|
||||
|
||||
**最后更新**: 2026-03-09
|
||||
|
||||
@@ -50,6 +50,7 @@ graph LR
|
||||
- Manual subscription activation via Telegram DM.
|
||||
- Small, focused paid Telegram channel for signal tests.
|
||||
- Invitation-only Web Access (Vercel).
|
||||
- Validate city-specific edge before scaling, with Ankara as the flagship strategy city.
|
||||
|
||||
### 🛠️ Phase 2: Automation (USDC)
|
||||
|
||||
@@ -66,6 +67,20 @@ graph LR
|
||||
- **Accuracy Leaderboard**: Monthly reports of DEB vs Market outcomes.
|
||||
- **Self-Serve Portal**: User dashboard for billing and alert settings.
|
||||
|
||||
### 📡 API Expansion Priority
|
||||
|
||||
- **P0-1 Market Layer**
|
||||
- Polymarket Gamma discovery + `py-clob-client` pricing / order book
|
||||
- **P0-2 Official Observation Layer**
|
||||
- Aviation Weather / METAR
|
||||
- weather.gov official forecast / observation / alert context
|
||||
- **P1 Lead Layer**
|
||||
- Ankara keeps Turkish MGM nearby network
|
||||
- U.S. cities may later receive Mesonet enhancement without replacing METAR
|
||||
- **P2 Product Layer**
|
||||
- Stripe / Polygon-USDC automation
|
||||
- Realtime sync and user entitlement system
|
||||
|
||||
---
|
||||
|
||||
## 🚧 Critical Constraints
|
||||
@@ -76,4 +91,4 @@ graph LR
|
||||
|
||||
---
|
||||
|
||||
**📅 Last Updated**: 2026-03-06
|
||||
**📅 Last Updated**: 2026-03-09
|
||||
|
||||
+5
-1
@@ -22,6 +22,7 @@ The core engine is stable, but several infrastructure "shortcut" decisions remai
|
||||
- [x] DEB Blending Algorithm
|
||||
- [x] Proactive Telegram Alert Engine
|
||||
- [x] Vercel Dashboard Infrastructure
|
||||
- [x] Legacy dashboard running behind Next.js/Vercel
|
||||
|
||||
---
|
||||
|
||||
@@ -32,6 +33,7 @@ The core engine is stable, but several infrastructure "shortcut" decisions remai
|
||||
| **Monolithic Bot** | `bot_listener.py` is hard to test and evolve. | Isolate UI interaction from business logic into `src/analysis`. |
|
||||
| **Subscription Store** | No persistent record of who has paid. | Migrate from in-memory user checks to **Supabase/PostgreSQL**. |
|
||||
| **Alert Transparency** | Operators cannot easily audit "why" an alert fired. | Add an `Evidence` metadata block to all internal alert payloads. |
|
||||
| **Encoding Drift** | Legacy frontend files have suffered mixed encodings.| Normalize all legacy static files to UTF-8 and stop editing them with incompatible encodings. |
|
||||
|
||||
---
|
||||
|
||||
@@ -42,6 +44,7 @@ The core engine is stable, but several infrastructure "shortcut" decisions remai
|
||||
| **Hard-coded Thresholds** | Modification requires code changes (e.g., 5s CD). | Extract all business constants into a structured `config.yaml`. |
|
||||
| **Simulation Harness** | No way to "replay" a rainy day to test alert logic. | Build a `ReplayEngine` using `data/daily_records.json`. |
|
||||
| **Backend Naming** | Artifacts of "market price" logic remain in naming. | Systematic refactor of variable names to reflect weather-intelligence focus. |
|
||||
| **Legacy Frontend Debt** | Large `public/static/app.js` mixes data, UI, charts, and modal logic. | Gradually extract panel, chart, and modal logic into typed modules without changing the current layout contract. |
|
||||
|
||||
---
|
||||
|
||||
@@ -59,7 +62,8 @@ The core engine is stable, but several infrastructure "shortcut" decisions remai
|
||||
1. **DB Integration**: Connect Supabase to `src/database/db_manager.py`.
|
||||
2. **Alert Transparency**: Append logic metrics (slope, lead delta) to push messages.
|
||||
3. **Authentication**: Secure `/api/city` on Vercel with subscription keys.
|
||||
4. **UTF-8 Cleanup**: Remove remaining mojibake from legacy static files and comments.
|
||||
|
||||
---
|
||||
|
||||
**📅 Last Updated**: 2026-03-06
|
||||
**📅 Last Updated**: 2026-03-09
|
||||
|
||||
+7
-1
@@ -18,6 +18,7 @@ Production URL:
|
||||
- Vercel serves the web UI
|
||||
- FastAPI on VPS serves API only
|
||||
- The old FastAPI static website has been removed
|
||||
- The production page shell is still the legacy dashboard embedded by `app/page.tsx`
|
||||
|
||||
Current request flow:
|
||||
- Browser -> Vercel frontend
|
||||
@@ -52,6 +53,11 @@ Thin BFF routes currently exposed by Next:
|
||||
- `GET /api/city/[name]`
|
||||
- `GET /api/history/[name]`
|
||||
|
||||
Current frontend behavior:
|
||||
- `/` keeps the world overview layout
|
||||
- City clicks stay inside the same layout and load the right-side panel
|
||||
- Future forecast dates open a modal instead of mutating the base panel
|
||||
|
||||
## Vercel Deployment
|
||||
|
||||
1. Import the repo into Vercel
|
||||
@@ -65,4 +71,4 @@ Thin BFF routes currently exposed by Next:
|
||||
- The page shell currently embeds the legacy dashboard HTML from `public/legacy/index.html`
|
||||
- If you change files under `public/static`, deploy to Vercel to make them live
|
||||
|
||||
Last updated: 2026-03-06
|
||||
Last updated: 2026-03-09
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
export default function HomePage() {
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<main className="h-screen w-screen overflow-hidden bg-black">
|
||||
<iframe
|
||||
title="PolyWeather Legacy Dashboard"
|
||||
src="/legacy/index.html?v=legacy-v3"
|
||||
src="/legacy/index.html?v=legacy-v15"
|
||||
className="h-full w-full border-0"
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,47 +1,41 @@
|
||||
<!DOCTYPE html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>PolyWeather — 天气衍生品智能地图</title>
|
||||
<meta name="description" content="PolyWeather 天气衍生品智能地图。实时 METAR、MGM、DEB 融合预报与 AI 分析。">
|
||||
<title>PolyWeather - 天气衍生品智能地图</title>
|
||||
<meta name="description" content="PolyWeather 天气衍生品智能地图,聚合 METAR、MGM、DEB、多模型预报与历史对账分析。">
|
||||
|
||||
<!-- Leaflet -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
|
||||
<!-- Google Fonts: Inter -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap"
|
||||
rel="stylesheet">
|
||||
|
||||
<link rel="stylesheet" href="/static/style.css?v=legacy-v3">
|
||||
<link rel="stylesheet" href="/static/style.css?v=legacy-v15">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- ── Map ── -->
|
||||
<div id="map"></div>
|
||||
|
||||
<!-- ── Header Overlay ── -->
|
||||
<header id="header">
|
||||
<div class="brand">
|
||||
<h1>🌡️ PolyWeather</h1>
|
||||
<h1>PolyWeather</h1>
|
||||
<span class="subtitle">天气衍生品智能分析</span>
|
||||
</div>
|
||||
<button class="info-btn" id="btnShowGuide" title="查看系统技术说明">ℹ️ 技术说明</button>
|
||||
<button class="info-btn" id="btnShowGuide" title="查看系统技术说明">技术说明</button>
|
||||
<div class="live-badge" id="liveBadge">
|
||||
<span class="pulse-dot"></span>
|
||||
<span>实时</span>
|
||||
</div>
|
||||
<button class="refresh-btn" id="refreshAllBtn" title="刷新所有数据">⟳</button>
|
||||
</div>
|
||||
<button class="refresh-btn" id="refreshAllBtn" title="刷新所有数据" aria-label="刷新所有数据">↻</button>
|
||||
</header>
|
||||
|
||||
<!-- ── City List Sidebar (left) ── -->
|
||||
<nav id="cityList" class="city-list">
|
||||
<div class="city-list-header">
|
||||
<span>🏙️ 监控城市</span>
|
||||
<span>监控城市</span>
|
||||
<span class="city-count" id="cityCount">0</span>
|
||||
</div>
|
||||
<div id="cityListItems" class="city-list-items">
|
||||
@@ -49,8 +43,7 @@
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- ── Detail Panel (right) ── -->
|
||||
<aside id="panel" class="detail-panel hidden">
|
||||
<aside id="panel" class="detail-panel hidden">
|
||||
<div class="panel-header">
|
||||
<button class="panel-close" id="panelClose">✕</button>
|
||||
<div class="panel-title-area">
|
||||
@@ -58,13 +51,12 @@
|
||||
<div class="panel-meta">
|
||||
<span id="panelRiskBadge" class="risk-badge">—</span>
|
||||
<span id="panelLocalTime" class="local-time">—</span>
|
||||
<button class="history-btn" id="btnShowHistory" title="查看历史记录与准确率">📊 历史对账</button>
|
||||
<button class="history-btn" id="btnShowHistory" title="查看历史记录与准确率">历史对账</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="panelContent" class="panel-body">
|
||||
<!-- ── Temperature Hero ── -->
|
||||
<section class="hero-section">
|
||||
<div class="hero-weather" id="heroWeather"></div>
|
||||
<div class="hero-temp">
|
||||
@@ -74,127 +66,111 @@
|
||||
<div class="hero-max-time" id="heroMaxTime"></div>
|
||||
<div class="hero-details">
|
||||
<div class="hero-item">
|
||||
<span class="label">📍 当前实测</span>
|
||||
<span class="label">当前实测</span>
|
||||
<span class="value" id="heroCurrent">—</span>
|
||||
</div>
|
||||
<div class="hero-item">
|
||||
<span class="label">🎯 WU 结算</span>
|
||||
<span class="label">WU 结算参考</span>
|
||||
<span class="value highlight" id="heroWU">—</span>
|
||||
</div>
|
||||
<div class="hero-item">
|
||||
<span class="label">🧬 DEB 预测</span>
|
||||
<span class="label">DEB 预测</span>
|
||||
<span class="value" id="heroDEB">—</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-sub" id="heroSub">
|
||||
<!-- obs time, cloud, wind -->
|
||||
</div>
|
||||
<div class="hero-sub" id="heroSub"></div>
|
||||
</section>
|
||||
|
||||
<!-- ── Trend Sparkline ── -->
|
||||
<section class="chart-section">
|
||||
<h3>📊 今日温度走势</h3>
|
||||
<h3>今日温度走势</h3>
|
||||
<div class="chart-wrapper">
|
||||
<canvas id="tempChart"></canvas>
|
||||
</div>
|
||||
<div class="chart-legend" id="chartLegend"></div>
|
||||
</section>
|
||||
|
||||
<!-- ── Probability Bars ── -->
|
||||
<section class="prob-section">
|
||||
<h3>🎲 结算概率分布</h3>
|
||||
<div id="probBars" class="prob-bars">
|
||||
<!-- Dynamically populated -->
|
||||
</div>
|
||||
<h3>结算概率分布</h3>
|
||||
<div id="probBars" class="prob-bars"></div>
|
||||
</section>
|
||||
|
||||
<!-- ── Multi-Model Comparison ── -->
|
||||
<section class="models-section">
|
||||
<h3>🔬 多模型预报</h3>
|
||||
<div id="modelBars" class="model-bars">
|
||||
<!-- Dynamically populated -->
|
||||
</div>
|
||||
<h3>多模型预报</h3>
|
||||
<div id="modelBars" class="model-bars"></div>
|
||||
</section>
|
||||
|
||||
<!-- ── Forecast Table ── -->
|
||||
<section class="forecast-section">
|
||||
<h3>📅 多日预报</h3>
|
||||
<h3>多日预报</h3>
|
||||
<div id="forecastTable" class="forecast-table"></div>
|
||||
<div class="sun-info" id="sunInfo"></div>
|
||||
</section>
|
||||
|
||||
<!-- ── AI Analysis ── -->
|
||||
<section class="ai-section">
|
||||
<h3>🤖 AI 深度分析</h3>
|
||||
<h3>AI 深度分析</h3>
|
||||
<div id="aiAnalysis" class="ai-box">
|
||||
<span class="ai-placeholder">点击城市后加载...</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<!-- ── Risk Profile ── -->
|
||||
<section class="risk-section">
|
||||
<h3>⚠️ 数据偏差风险</h3>
|
||||
<h3>数据偏差风险</h3>
|
||||
<div id="riskInfo" class="risk-info"></div>
|
||||
</section>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ── Loading Overlay ── -->
|
||||
<div id="loading" class="loading-overlay hidden">
|
||||
<div class="loading-spinner"></div>
|
||||
<span>正在获取气象数据,请稍候...</span>
|
||||
</div>
|
||||
|
||||
<!-- ── Technical Guide Modal ── -->
|
||||
<!-- 鈹€鈹€ Technical Guide Modal 鈹€鈹€ -->
|
||||
<div id="guideModal" class="modal-overlay hidden">
|
||||
<div class="modal-content large">
|
||||
<div class="modal-header">
|
||||
<h2>📚 PolyWeather 系统技术说明</h2>
|
||||
<button class="modal-close" id="guideModalClose">✕</button>
|
||||
<h2>📚 PolyWeather 系统技术说明</h2>
|
||||
<button class="modal-close" id="guideModalClose">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="guide-grid">
|
||||
<div class="guide-card">
|
||||
<h3>🧬 DEB 动态融合预测</h3>
|
||||
<p><b>Dynamic Ensemble Blending</b> 是系统的核心算法。它不只是简单的平均值,而是根据各个模型(ECMWF, GFS, MGM等)在**过去 30
|
||||
天**内的实际表现,动态分配权重。表现越稳的模型,在最终决策中占比越高。</p>
|
||||
<h3>🧮 DEB 动态融合预测</h3>
|
||||
<p><b>Dynamic Ensemble Blending</b> 是系统的核心预测层。它不是对 ECMWF、GFS、ICON、GEM、JMA 等模型的简单平均,而是结合最近样本表现、当前实况与城市偏置后得到的动态加权结果。</p>
|
||||
</div>
|
||||
<div class="guide-card">
|
||||
<h3>🎲 结算概率引擎 (μ)</h3>
|
||||
<p>基于正态分布模型。我们提取所有预报模型的离散度作为**不确定性系数 ($\sigma$)**,结合 DEB 预测值作为**期望值 ($\mu$)**。这能告诉你某个温度区间(如 10°C
|
||||
盘口)发生的真实概率,辅助对冲决策。</p>
|
||||
<h3>🎯 结算概率引擎</h3>
|
||||
<p>右侧的结算概率分布基于 DEB 预测值与多模型离散度动态计算。μ代表当前分布中心,会随着模型、实况和时间变化而变动,不是固定预报值。</p>
|
||||
</div>
|
||||
<div class="guide-card">
|
||||
<h3>📍 结算点 (Airport) 逻辑</h3>
|
||||
<p>Polymarket 结算是以**机场 METAR
|
||||
气象站**为准。在安卡拉等复杂城市,我们同时监控**市中心总站**(潜力热岛)和**机场站**(物理结算点)。当两者温差巨大时,往往预示着随后的补涨跳升机会。</p>
|
||||
<h3>✈️ 结算点与主观测源</h3>
|
||||
<p>Polymarket 结算逻辑以机场 METAR 为主。系统现在优先使用 Aviation Weather API 的机场报文与原始 METAR,并区分观测时间与接收时间,避免把发布延迟误认成温度变化。</p>
|
||||
</div>
|
||||
<div class="guide-card">
|
||||
<h3>⚠️ 风险偏置档案</h3>
|
||||
<p>系统记录了每个城市机场站与市区的距离、海拔差及季度性偏差。例如,首尔仁川机场因靠海,最高温通常比市区偏低;慕尼黑机场因海拔高,夜间降温极快。这些偏置已自动整合进 AI 分析逻辑中。
|
||||
</p>
|
||||
<h3>📍 Ankara 专属增强</h3>
|
||||
<p>Ankara 不走通用城市逻辑。结算主站以 LTAC / Esenboğa 为准,周边领先信号优先参考 Turkish MGM 站网,其中 <b>Ankara (Bölge/Center)</b> 是重点监控站,不由 Etimesgut 代替。</p>
|
||||
</div>
|
||||
<div class="guide-card">
|
||||
<h3>📈 未来日期概率分布</h3>
|
||||
<p>当你切换到未来 3-5 天时,系统会基于各模型对该日期的预报分歧度重新计算概率。分歧越大,分布越扁平(风险高);共识越强,分布越尖锐(机会明朗)。</p>
|
||||
<h3>📆 未来日期分析</h3>
|
||||
<p>点击多日预报后打开的模态框,主要用于分析下一个交易日。 6-48 小时趋势以 weather.gov 和 Open-Meteo 为主,部分城市可会补充 Meteoblue; 0-2 小时临近判断则优先看 METAR 与周边站。</p>
|
||||
</div>
|
||||
<div class="guide-card">
|
||||
<h3>📊 历史对账规则</h3>
|
||||
<p>历史准确率对账只统计已结算的样本。网页端采用近15天滚动视图,机器人 /deb 报告采用近7天窗口,不把当天未结算的样本算入胜率和 MAE。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="guide-footer">
|
||||
<p>※ 数据来源:NOAA METAR, Turkish MGM, Open-Meteo Ensemble, Meteoblue, NWS.</p>
|
||||
<p>※ 数据源以 Aviation Weather / METAR、Turkish MGM、Open-Meteo、weather.gov 为主,部分城市补充 Meteoblue。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── History Chart Modal ── -->
|
||||
<!-- 鈹€鈹€ History Chart Modal 鈹€鈹€ -->
|
||||
<div id="historyModal" class="modal-overlay hidden">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>📊 <span id="historyModalTitle">历史准确率</span></h2>
|
||||
<button class="modal-close" id="historyModalClose">✕</button>
|
||||
<h2>📊 <span id="historyModalTitle">历史准确率对账</span></h2>
|
||||
<button class="modal-close" id="historyModalClose">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="history-stats" id="historyStats"></div>
|
||||
@@ -205,13 +181,61 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="futureForecastModal" class="modal-overlay hidden">
|
||||
<div class="modal-content large future-modal">
|
||||
<div class="modal-header">
|
||||
<h2>📆 <span id="futureForecastTitle">未来日期分析</span></h2>
|
||||
<button class="modal-close" id="futureForecastClose" aria-label="关闭未来日期分析">✕</button>
|
||||
</div>
|
||||
<div class="modal-body future-modal-body">
|
||||
<div class="history-stats" id="futureForecastStats"></div>
|
||||
|
||||
<section class="future-modal-section">
|
||||
<h3>📈 温度走势</h3>
|
||||
<div class="history-chart-wrapper future-chart-wrapper">
|
||||
<canvas id="futureForecastChart"></canvas>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="future-modal-grid">
|
||||
<section class="future-modal-section">
|
||||
<h3>🎯 结算概率分布</h3>
|
||||
<div id="futureProbBars" class="prob-bars future-prob-bars"></div>
|
||||
</section>
|
||||
|
||||
<section class="future-modal-section">
|
||||
<h3>🧮 多模型预报</h3>
|
||||
<div id="futureModelBars" class="model-bars"></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="future-modal-grid">
|
||||
<section class="future-modal-section">
|
||||
<h3><span class="section-inline-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19V5"/><path d="M10 19V10"/><path d="M16 19V7"/><path d="M22 19V13"/></svg></span>未来 6-48 小时趋势</h3>
|
||||
<div id="futureTrendGrid" class="future-trend-grid"></div>
|
||||
</section>
|
||||
|
||||
<section class="future-modal-section">
|
||||
<h3>⏱ 未来 0-2 小时临近判断</h3>
|
||||
<div id="futureNowcast" class="future-text-block"></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Leaflet JS -->
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<!-- Leaflet Heatmap Plugin -->
|
||||
<script src="https://leaflet.github.io/Leaflet.heat/dist/leaflet-heat.js"></script>
|
||||
<!-- Chart.js -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
|
||||
<script src="/static/app.js?v=legacy-v3"></script>
|
||||
<script src="/static/app.js?v=legacy-v15"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+928
-453
File diff suppressed because it is too large
Load Diff
@@ -1148,9 +1148,11 @@ body {
|
||||
border-radius: 16px;
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
max-height: calc(100vh - 48px);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
@@ -1180,6 +1182,7 @@ body {
|
||||
|
||||
.modal-body {
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.history-stats {
|
||||
@@ -1242,6 +1245,173 @@ body {
|
||||
max-width: 900px;
|
||||
}
|
||||
|
||||
.modal-content.large.future-modal {
|
||||
width: min(96vw, 1720px);
|
||||
max-width: 1720px;
|
||||
}
|
||||
|
||||
.future-modal-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
padding-right: 14px;
|
||||
}
|
||||
|
||||
.future-modal-section {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: 14px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.future-modal-section h3 {
|
||||
margin: 0 0 14px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.section-inline-icon {
|
||||
display: inline-flex;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--accent-cyan);
|
||||
flex: 0 0 16px;
|
||||
}
|
||||
|
||||
.section-inline-icon svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.future-modal-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.future-chart-wrapper {
|
||||
height: 220px;
|
||||
}
|
||||
|
||||
.future-prob-bars .prob-bar-fill {
|
||||
min-width: 56px;
|
||||
}
|
||||
|
||||
.future-trend-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.future-trend-card {
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
background: rgba(255, 255, 255, 0.025);
|
||||
}
|
||||
|
||||
.future-trend-label {
|
||||
display: block;
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.future-trend-value {
|
||||
display: block;
|
||||
color: var(--text-primary);
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.future-trend-note {
|
||||
margin-top: 6px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.future-trend-value.warm {
|
||||
color: #34d399;
|
||||
}
|
||||
|
||||
.future-trend-value.cold {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.future-text-block {
|
||||
color: var(--text-secondary);
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.future-text-block strong {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.future-front-score {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.future-front-bar {
|
||||
position: relative;
|
||||
height: 10px;
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(90deg, rgba(245, 158, 11, 0.35), rgba(255,255,255,0.06) 50%, rgba(52, 211, 153, 0.35));
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.future-front-bar::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: var(--score-position, 50%);
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 999px;
|
||||
background: var(--accent-cyan);
|
||||
transform: translate(-50%, -50%);
|
||||
box-shadow: 0 0 0 4px rgba(34, 211, 238, 0.14);
|
||||
}
|
||||
|
||||
.future-front-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.future-front-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border-subtle);
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.future-modal-grid,
|
||||
.future-trend-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.future-trend-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.guide-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
|
||||
@@ -790,7 +790,9 @@ class WeatherDataCollector:
|
||||
points_resp.raise_for_status()
|
||||
points_data = points_resp.json()
|
||||
|
||||
forecast_url = points_data.get("properties", {}).get("forecast")
|
||||
properties = points_data.get("properties", {})
|
||||
forecast_url = properties.get("forecast")
|
||||
hourly_url = properties.get("forecastHourly")
|
||||
if not forecast_url:
|
||||
return None
|
||||
|
||||
@@ -805,6 +807,41 @@ class WeatherDataCollector:
|
||||
if not periods:
|
||||
return None
|
||||
|
||||
hourly_periods = []
|
||||
if hourly_url:
|
||||
hourly_resp = self.session.get(
|
||||
hourly_url, headers=headers, timeout=self.timeout
|
||||
)
|
||||
hourly_resp.raise_for_status()
|
||||
hourly_data = hourly_resp.json()
|
||||
hourly_periods = hourly_data.get("properties", {}).get("periods", [])[:48]
|
||||
|
||||
active_alerts = []
|
||||
try:
|
||||
alerts_resp = self.session.get(
|
||||
"https://api.weather.gov/alerts/active",
|
||||
params={"point": f"{lat},{lon}"},
|
||||
headers=headers,
|
||||
timeout=self.timeout,
|
||||
)
|
||||
alerts_resp.raise_for_status()
|
||||
alerts_data = alerts_resp.json()
|
||||
for feature in alerts_data.get("features", [])[:8]:
|
||||
ap = feature.get("properties", {})
|
||||
active_alerts.append(
|
||||
{
|
||||
"event": ap.get("event"),
|
||||
"headline": ap.get("headline"),
|
||||
"severity": ap.get("severity"),
|
||||
"certainty": ap.get("certainty"),
|
||||
"urgency": ap.get("urgency"),
|
||||
"effective": ap.get("effective"),
|
||||
"ends": ap.get("ends"),
|
||||
}
|
||||
)
|
||||
except Exception:
|
||||
active_alerts = []
|
||||
|
||||
# 3. 提取今日最高温(找 isDaytime=True 的第一个)
|
||||
today_high = None
|
||||
for p in periods:
|
||||
@@ -822,6 +859,36 @@ class WeatherDataCollector:
|
||||
"source": "nws",
|
||||
"today_high": today_high,
|
||||
"unit": "fahrenheit",
|
||||
"forecast_periods": [
|
||||
{
|
||||
"name": p.get("name"),
|
||||
"start_time": p.get("startTime"),
|
||||
"end_time": p.get("endTime"),
|
||||
"is_daytime": p.get("isDaytime"),
|
||||
"temperature": p.get("temperature"),
|
||||
"temperature_trend": p.get("temperatureTrend"),
|
||||
"wind_speed": p.get("windSpeed"),
|
||||
"wind_direction": p.get("windDirection"),
|
||||
"short_forecast": p.get("shortForecast"),
|
||||
"detailed_forecast": p.get("detailedForecast"),
|
||||
"precipitation_probability": (p.get("probabilityOfPrecipitation") or {}).get("value"),
|
||||
}
|
||||
for p in periods[:14]
|
||||
],
|
||||
"hourly_periods": [
|
||||
{
|
||||
"start_time": p.get("startTime"),
|
||||
"end_time": p.get("endTime"),
|
||||
"temperature": p.get("temperature"),
|
||||
"temperature_unit": p.get("temperatureUnit"),
|
||||
"wind_speed": p.get("windSpeed"),
|
||||
"wind_direction": p.get("windDirection"),
|
||||
"short_forecast": p.get("shortForecast"),
|
||||
"precipitation_probability": (p.get("probabilityOfPrecipitation") or {}).get("value"),
|
||||
}
|
||||
for p in hourly_periods
|
||||
],
|
||||
"active_alerts": active_alerts,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.warning(f"NWS 请求失败: {e}")
|
||||
@@ -849,7 +916,7 @@ class WeatherDataCollector:
|
||||
"latitude": lat,
|
||||
"longitude": lon,
|
||||
"current_weather": "true",
|
||||
"hourly": "temperature_2m,shortwave_radiation",
|
||||
"hourly": "temperature_2m,shortwave_radiation,dew_point_2m,pressure_msl,wind_speed_10m,wind_direction_10m,precipitation_probability,cloud_cover",
|
||||
"daily": "temperature_2m_max,apparent_temperature_max,sunrise,sunset,sunshine_duration",
|
||||
"timezone": "auto",
|
||||
"forecast_days": forecast_days,
|
||||
@@ -1436,16 +1503,14 @@ class WeatherDataCollector:
|
||||
# 获取时区偏移以过滤 METAR
|
||||
utc_offset = open_meteo.get("utc_offset", 0)
|
||||
|
||||
# 对伦敦,获取 Meteoblue 预测 (公认最准)
|
||||
if city_lower == "london":
|
||||
mb_data = self.fetch_from_meteoblue(
|
||||
lat,
|
||||
lon,
|
||||
timezone_name=open_meteo.get("timezone", "UTC"),
|
||||
use_fahrenheit=use_fahrenheit,
|
||||
)
|
||||
if mb_data:
|
||||
results["meteoblue"] = mb_data
|
||||
mb_data = self.fetch_from_meteoblue(
|
||||
lat,
|
||||
lon,
|
||||
timezone_name=open_meteo.get("timezone", "UTC"),
|
||||
use_fahrenheit=use_fahrenheit,
|
||||
)
|
||||
if mb_data:
|
||||
results["meteoblue"] = mb_data
|
||||
|
||||
# 对美国城市,额外获取 NWS 高精预报
|
||||
if use_fahrenheit:
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
const fs = require('fs');
|
||||
|
||||
function replaceRegex(content, regex, replacement, label) {
|
||||
const next = content.replace(regex, replacement);
|
||||
if (next === content) {
|
||||
console.error('No match for', label);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
const appJsPath = 'E:/web/PolyWeather/frontend/public/static/app.js';
|
||||
let app = fs.readFileSync(appJsPath, 'utf8');
|
||||
|
||||
app = replaceRegex(app, /function updateCityListInfo\(cityData\) \{[\s\S]*?\n\}/, `function updateCityListInfo(cityData) {
|
||||
const cityName = cityData.name;
|
||||
const cityId = cityName.replace(/\\s/g, "-");
|
||||
const temp = cityData.current?.temp;
|
||||
|
||||
const tempEl = document.getElementById(\`temp-\${cityId}\`);
|
||||
if (tempEl && temp != null) {
|
||||
tempEl.textContent = \`\${temp}\${cityData.temp_symbol}\`;
|
||||
tempEl.classList.add("loaded");
|
||||
}
|
||||
|
||||
const timeEl = document.getElementById(\`time-\${cityId}\`);
|
||||
if (timeEl && cityData.local_time) {
|
||||
timeEl.textContent = \`🕐 \${cityData.local_time}\`;
|
||||
}
|
||||
|
||||
const maxEl = document.getElementById(\`max-\${cityId}\`);
|
||||
if (maxEl && cityData.current?.max_temp_time) {
|
||||
maxEl.textContent = \`峰值 @\${cityData.current.max_temp_time}\`;
|
||||
}
|
||||
}`,'updateCityListInfo');
|
||||
|
||||
app = app.replace(/alert\(`[^`]*cityName[^`]*`\);/, 'alert(`加载 ${cityName} 数据失败:${e.message}`);');
|
||||
|
||||
app = replaceRegex(app, /function renderPanel\(data\) \{[\s\S]*?\n\}/, `function renderPanel(data) {
|
||||
const panel = document.getElementById("panel");
|
||||
if (!panel) return;
|
||||
panel.classList.remove("hidden");
|
||||
requestAnimationFrame(() => panel.classList.add("visible"));
|
||||
|
||||
const panelCityName = document.getElementById("panelCityName");
|
||||
const panelLocalTime = document.getElementById("panelLocalTime");
|
||||
const badge = document.getElementById("panelRiskBadge");
|
||||
|
||||
if (panelCityName) {
|
||||
panelCityName.textContent = \`\${data.risk?.emoji || "🏙️"} \${data.display_name}\`;
|
||||
}
|
||||
if (panelLocalTime) {
|
||||
panelLocalTime.textContent = \`🕐 \${data.local_time || "--:--"} 当地时间\`;
|
||||
}
|
||||
if (badge) {
|
||||
badge.textContent = {
|
||||
high: "🔴 高危",
|
||||
medium: "🟡 中危",
|
||||
low: "🟢 低危",
|
||||
}[data.risk?.level] || "未知";
|
||||
badge.className = \`risk-badge \${data.risk?.level || "low"}\`;
|
||||
}
|
||||
|
||||
renderHero(data);
|
||||
renderChart(data);
|
||||
renderProbabilities(data);
|
||||
if (!selectedForecastDate) {
|
||||
selectedForecastDate = data.local_date;
|
||||
}
|
||||
renderModels(data);
|
||||
renderForecast(data);
|
||||
renderAI(data);
|
||||
renderRisk(data);
|
||||
}`,'renderPanel');
|
||||
|
||||
app = replaceRegex(app, /const METAR_WX_MAP = \{[\s\S]*?\n\};/, `const METAR_WX_MAP = {
|
||||
RA: { label: "降雨", icon: "🌧️" },
|
||||
"-RA": { label: "小雨", icon: "🌦️" },
|
||||
"+RA": { label: "强降雨", icon: "⛈️" },
|
||||
SN: { label: "降雪", icon: "❄️" },
|
||||
"-SN": { label: "小雪", icon: "🌨️" },
|
||||
"+SN": { label: "大雪", icon: "🌨️" },
|
||||
DZ: { label: "毛毛雨", icon: "🌦️" },
|
||||
FG: { label: "雾", icon: "🌫️" },
|
||||
BR: { label: "薄雾", icon: "🌫️" },
|
||||
HZ: { label: "霾", icon: "🌫️" },
|
||||
TS: { label: "雷暴", icon: "⛈️" },
|
||||
VCTS: { label: "附近雷暴", icon: "⛈️" },
|
||||
SQ: { label: "飑", icon: "💨" },
|
||||
GS: { label: "冰雹", icon: "🌨️" },
|
||||
};`, 'METAR_WX_MAP');
|
||||
|
||||
app = app.replace(/return \{ label: code, icon: .*? \};/, 'return { label: code, icon: "🌤️" };');
|
||||
|
||||
app = replaceRegex(app, /function renderHero\(data\) \{[\s\S]*?document.getElementById\("heroWeather"\)\.innerHTML = `/, `function renderHero(data) {
|
||||
const cur = data.current || {};
|
||||
const sym = data.temp_symbol || "°C";
|
||||
const displayTemp = cur.temp;
|
||||
|
||||
let weatherText = cur.cloud_desc || "未知";
|
||||
let weatherIcon =
|
||||
{
|
||||
多云: "☁️",
|
||||
阴天: "☁️",
|
||||
少云: "🌤️",
|
||||
散云: "☁️",
|
||||
晴: "☀️",
|
||||
晴朗: "☀️",
|
||||
}[cur.cloud_desc] || "🌤️";
|
||||
|
||||
if (cur.wx_desc) {
|
||||
const metarTranslation = translateMETAR(cur.wx_desc);
|
||||
if (metarTranslation) {
|
||||
weatherText = metarTranslation.label;
|
||||
weatherIcon = metarTranslation.icon;
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("heroWeather").innerHTML = ``, 'renderHeroPrefix');
|
||||
|
||||
fs.writeFileSync(appJsPath, app, 'utf8');
|
||||
|
||||
const pagePath = 'E:/web/PolyWeather/frontend/app/page.tsx';
|
||||
let page = fs.readFileSync(pagePath, 'utf8');
|
||||
page = page.replace(/legacy-v13/g, 'legacy-v14');
|
||||
fs.writeFileSync(pagePath, page, 'utf8');
|
||||
|
||||
const htmlPath = 'E:/web/PolyWeather/frontend/public/legacy/index.html';
|
||||
let html = fs.readFileSync(htmlPath, 'utf8');
|
||||
html = html.replace(/legacy-v13/g, 'legacy-v14');
|
||||
fs.writeFileSync(htmlPath, html, 'utf8');
|
||||
+59
@@ -262,6 +262,12 @@ def _analyze(city: str, force_refresh: bool = False) -> Dict[str, Any]:
|
||||
h_times = hourly.get("time", [])
|
||||
h_temps = hourly.get("temperature_2m", [])
|
||||
h_rad = hourly.get("shortwave_radiation", [])
|
||||
h_dew = hourly.get("dew_point_2m", [])
|
||||
h_pressure = hourly.get("pressure_msl", [])
|
||||
h_wspd = hourly.get("wind_speed_10m", [])
|
||||
h_wdir = hourly.get("wind_direction_10m", [])
|
||||
h_precip_prob = hourly.get("precipitation_probability", [])
|
||||
h_cloud_cover = hourly.get("cloud_cover", [])
|
||||
|
||||
peak_hours = []
|
||||
if h_times and h_temps and om_today is not None:
|
||||
@@ -326,6 +332,54 @@ def _analyze(city: str, force_refresh: bool = False) -> Dict[str, Any]:
|
||||
today_hourly["temps"].append(h_temps[i] if i < len(h_temps) else None)
|
||||
today_hourly["radiation"].append(h_rad[i] if i < len(h_rad) else None)
|
||||
|
||||
# ── 12b. Next 48h hourly block for future-date analysis modal ──
|
||||
next_48h_hourly = {
|
||||
"times": [],
|
||||
"temps": [],
|
||||
"radiation": [],
|
||||
"dew_point": [],
|
||||
"pressure_msl": [],
|
||||
"wind_speed_10m": [],
|
||||
"wind_direction_10m": [],
|
||||
"precipitation_probability": [],
|
||||
"cloud_cover": [],
|
||||
}
|
||||
try:
|
||||
local_anchor = datetime.strptime(
|
||||
f"{local_date_str} {local_time_str}", "%Y-%m-%d %H:%M"
|
||||
)
|
||||
except Exception:
|
||||
local_anchor = None
|
||||
|
||||
if local_anchor is not None:
|
||||
horizon = local_anchor + timedelta(hours=48)
|
||||
for i, ts in enumerate(h_times):
|
||||
try:
|
||||
ts_dt = datetime.fromisoformat(ts)
|
||||
except Exception:
|
||||
continue
|
||||
if ts_dt < local_anchor or ts_dt > horizon:
|
||||
continue
|
||||
next_48h_hourly["times"].append(ts)
|
||||
next_48h_hourly["temps"].append(h_temps[i] if i < len(h_temps) else None)
|
||||
next_48h_hourly["radiation"].append(h_rad[i] if i < len(h_rad) else None)
|
||||
next_48h_hourly["dew_point"].append(h_dew[i] if i < len(h_dew) else None)
|
||||
next_48h_hourly["pressure_msl"].append(
|
||||
h_pressure[i] if i < len(h_pressure) else None
|
||||
)
|
||||
next_48h_hourly["wind_speed_10m"].append(
|
||||
h_wspd[i] if i < len(h_wspd) else None
|
||||
)
|
||||
next_48h_hourly["wind_direction_10m"].append(
|
||||
h_wdir[i] if i < len(h_wdir) else None
|
||||
)
|
||||
next_48h_hourly["precipitation_probability"].append(
|
||||
h_precip_prob[i] if i < len(h_precip_prob) else None
|
||||
)
|
||||
next_48h_hourly["cloud_cover"].append(
|
||||
h_cloud_cover[i] if i < len(h_cloud_cover) else None
|
||||
)
|
||||
|
||||
# ── 13. Cloud description (METAR primary, MGM fallback) ──
|
||||
clouds = mc.get("clouds", [])
|
||||
cloud_desc = ""
|
||||
@@ -512,6 +566,10 @@ def _analyze(city: str, force_refresh: bool = False) -> Dict[str, Any]:
|
||||
"sunset": sunset,
|
||||
"sunshine_hours": sunshine_h,
|
||||
},
|
||||
"source_forecasts": {
|
||||
"weather_gov": raw.get("nws") or {},
|
||||
"meteoblue": raw.get("meteoblue") or {},
|
||||
},
|
||||
"multi_model": {k: v for k, v in current_forecasts.items() if v is not None},
|
||||
"multi_model_daily": multi_model_daily,
|
||||
"deb": {"prediction": deb_val, "weights_info": deb_weights},
|
||||
@@ -528,6 +586,7 @@ def _analyze(city: str, force_refresh: bool = False) -> Dict[str, Any]:
|
||||
"status": peak_status,
|
||||
},
|
||||
"hourly": today_hourly,
|
||||
"hourly_next_48h": next_48h_hourly,
|
||||
"metar_today_obs": [
|
||||
{"time": t, "temp": v}
|
||||
for t, v in (metar.get("today_obs", []) if metar else [])
|
||||
|
||||
Reference in New Issue
Block a user