From 5f04506aade7fa7d5e0c81da700e829f0f16faf4 Mon Sep 17 00:00:00 2001
From: "2569718930@qq.com" <2569718930@qq.com>
Date: Thu, 5 Mar 2026 16:33:00 +0800
Subject: [PATCH] feat: Implement PolyWeather web map API with FastAPI,
integrating existing weather data collection and analysis modules.
---
bot_listener.py | 82 ++------
config/config.yaml | 17 +-
docs/POLYMUSIC_INDEPENDENT_DOC.md | 87 --------
src/data_collection/city_registry.py | 241 ++++++++++++++++++++++
src/data_collection/city_risk_profiles.py | 179 ++--------------
src/data_collection/weather_sources.py | 67 +++---
web/app.py | 37 ++--
7 files changed, 328 insertions(+), 382 deletions(-)
delete mode 100644 docs/POLYMUSIC_INDEPENDENT_DOC.md
create mode 100644 src/data_collection/city_registry.py
diff --git a/bot_listener.py b/bot_listener.py
index 5bcaefc2..9af0b78a 100644
--- a/bot_listener.py
+++ b/bot_listener.py
@@ -63,24 +63,9 @@ def start_bot():
)
return
+ from src.data_collection.city_registry import ALIASES, CITY_REGISTRY
city_input = parts[1].strip().lower()
- # 复用城市名映射
- city_aliases = {
- "ank": "ankara",
- "lon": "london",
- "par": "paris",
- "nyc": "new york",
- "chi": "chicago",
- "dal": "dallas",
- "mia": "miami",
- "atl": "atlanta",
- "sea": "seattle",
- "tor": "toronto",
- "sel": "seoul",
- "ba": "buenos aires",
- "wel": "wellington",
- }
- city_name = city_aliases.get(city_input, city_input)
+ city_name = ALIASES.get(city_input, city_input)
from src.analysis.deb_algorithm import load_history
import os as _os
@@ -262,75 +247,38 @@ def start_bot():
)
return
+ from src.data_collection.city_registry import ALIASES, CITY_REGISTRY
city_input = parts[1].strip().lower()
+
+ # --- 使用统一注册表解析城市 ---
+ SUPPORTED_CITIES = list(CITY_REGISTRY.keys())
- # --- 核心标准名称映射表 ---
- # 这里的 Key 是缩写或别名,Value 是 Open-Meteo 识别的标准全称
- STANDARD_MAPPING = {
- "sel": "seoul",
- "seo": "seoul",
- "首尔": "seoul",
- "lon": "london",
- "伦敦": "london",
- "tor": "toronto",
- "多伦多": "toronto",
- "ank": "ankara",
- "安卡拉": "ankara",
- "wel": "wellington",
- "惠灵顿": "wellington",
- "ba": "buenos aires",
- "布宜诺斯艾利斯": "buenos aires",
- "nyc": "new york",
- "ny": "new york",
- "纽约": "new york",
- "chi": "chicago",
- "芝加哥": "chicago",
- "sea": "seattle",
- "西雅图": "seattle",
- "mia": "miami",
- "迈阿密": "miami",
- "atl": "atlanta",
- "亚特兰大": "atlanta",
- "dal": "dallas",
- "达拉斯": "dallas",
- "la": "los angeles",
- "洛杉矶": "los angeles",
- "par": "paris",
- "巴黎": "paris",
- }
-
- # 支持的城市全名列表(用于模糊匹配)
- SUPPORTED_CITIES = list(set(STANDARD_MAPPING.values()))
-
- # 1. 第一优先级:严格全字匹配(别名/缩写)
- city_name = STANDARD_MAPPING.get(city_input)
-
- # 2. 第二优先级:输入本身就是城市全名
+ # 1. 第一优先级:全称或别名完全匹配
+ city_name = ALIASES.get(city_input)
if not city_name and city_input in SUPPORTED_CITIES:
city_name = city_input
- # 3. 第三优先级:前缀匹配(在别名和城市全名中搜索)
+ # 2. 第二优先级:前缀模糊匹配
if not city_name and len(city_input) >= 2:
- # 先搜别名
- for k, v in STANDARD_MAPPING.items():
+ # 搜别名
+ for k, v in ALIASES.items():
if k.startswith(city_input):
city_name = v
break
- # 再搜城市全名
+ # 搜城市全名
if not city_name:
for full_name in SUPPORTED_CITIES:
if full_name.startswith(city_input):
city_name = full_name
break
- # 4. 未找到 → 报错,列出支持的城市
+ # 3. 未找到 → 报错
if not city_name:
- city_list = ", ".join(sorted(set(STANDARD_MAPPING.values())))
+ city_list = ", ".join(sorted(SUPPORTED_CITIES))
bot.reply_to(
message,
f"❌ 未找到城市: {city_input}\n\n"
- f"支持的城市: {city_list}\n\n"
- f"也可以用缩写,如 /city dal 查达拉斯",
+ f"支持的城市: {city_list}",
parse_mode="HTML",
)
return
diff --git a/config/config.yaml b/config/config.yaml
index d8c6744c..4ad96862 100644
--- a/config/config.yaml
+++ b/config/config.yaml
@@ -1,6 +1,6 @@
# Weather API Configuration
weather:
- meteoblue_api_key: null # Set via METEOBLUE_API_KEY env var
+ meteoblue_api_key: null # Set via METEOBLUE_API_KEY env var
timeout: 30
# Target Cities
@@ -30,6 +30,21 @@ cities:
country: "USA"
latitude: 41.8781
longitude: -87.6298
+ - id: "lucknow"
+ city: "Lucknow"
+ country: "India"
+ latitude: 26.7606
+ longitude: 80.8893
+ - id: "sao paulo"
+ city: "São Paulo"
+ country: "Brazil"
+ latitude: -23.4356
+ longitude: -46.4731
+ - id: "munich"
+ city: "Munich"
+ country: "Germany"
+ latitude: 48.3538
+ longitude: 11.7861
# Logging
logging:
diff --git a/docs/POLYMUSIC_INDEPENDENT_DOC.md b/docs/POLYMUSIC_INDEPENDENT_DOC.md
deleted file mode 100644
index 12a0a6df..00000000
--- a/docs/POLYMUSIC_INDEPENDENT_DOC.md
+++ /dev/null
@@ -1,87 +0,0 @@
-# 🎵 PolyMusic: 音乐市场量化分析系统 - 独立开发文档
-
-## 1. 项目定位
-
-PolyMusic 是一个独立的量化分析系统,旨在通过聚合 **流媒体实测数据 (Spotify/Apple Music)**、**社交媒体前置信号 (TikTok/YouTube)** 和 **行业动态 (Awards/Releases)**,为音乐相关的预测市场提供精准的决策分析。
-
----
-
-## 2. 核心架构设计
-
-### A. 数据采集层 (The Data Feed)
-
-- **Spotify Scanner**:
- - 每日定时抓取 Global & US Top 200 榜单。
- - 提取字段:Track Name, Artist, Daily Streams, Position, Previous Position.
-- **Viral Tracker (前哨信号)**:
- - 监控 TikTok 热门音频趋势图,追踪 BGM 使用量增幅 (Acceleration)。
- - 监控 YouTube Trending 榜单。
-- **Contextual Hub (行业动态)**:
- - 重大活动日历:超级碗、格莱美、科切拉音乐节、主流艺人回归预热。
-
-### B. 分析引擎 (The Trend Engine)
-
-- **Accumulation Solver (积分缺口分析)**:
- - 专门针对“周榜”市场。根据当前已消耗的天数,计算出各候选人要反超第一名所需的每日平均流值及其标准差。
-- **Decay Controller (热度半衰期计算)**:
- - 为突发热点(如夺金表演、空降 MV)建立衰减曲线记录。判断当前的高流值是“单日冲击”还是“长期阶跃”。
-- **Consensus Tracker**:
- - 对比 Kworb, Billboard 预测与 Polymarket 赔率的偏差 (MAE)。
-
-### C. AI 决策层 (Groq LLaMA 3.3 70B)
-
-- **Prompt 逻辑框架**:
- - **P0 (Event Trigger)**: 是否有头部艺人突然在 Instagram/Twitter 进行大规模预热或空降。
- - **P1 (Viral Drift)**: 该歌曲的传播是否已由于某个非音乐事件(体育、电影、社交媒体挑战)出现斜率阶跃。
- - **P2 (Consistency Check)**: 预测市场的价格波动是否与目前观察到的流值增长率相匹配。
-
----
-
-## 3. 技术栈建议
-
-- **Backend**: Python 3.11+ (FastAPI)
-- **Database**: SQLite (存储每日流值与历史 MAE)
-- **AI Engine**: Groq SDK (LLaMA 3.3 70B)
-- **Crawler**: Requests / BeautifulSoup / Selenium (用于绕过部分动态频率限制)
-
----
-
-## 4. 目录结构预览 (Independent Project)
-
-```text
-PolyMusic/
-├── src/
-│ ├── data/ # 爬虫与数据采集
-│ │ ├── spotify.py
-│ │ ├── tiktok.py
-│ │ └── billboard.py
-│ ├── analysis/ # 核心算法
-│ │ ├── accumulation.py
-│ │ └── decay_model.py
-│ └── ai/ # AI 决策管线
-│ └── prompt_engine.py
-├── data/ # 本地存储 (JSON/SQLite)
-├── web/ # 可视化面板 (类似于 PolyWeather Map)
-├── bot_listener.py # Telegram 交互入口
-└── requirements.txt
-```
-
----
-
-## 5. 核心博弈指标 (KPIs)
-
-- **Stream Gap (流值缺口)**:反超所需最低日均流值。
-- **Momentum Coefficient (动能系数)**:流值增长的二阶导数。
-- **Price Inaccuracy (定价误差)**:Polymarket 赔率与量化结果的期望偏差。
-
----
-
-## 6. 下一步动作
-
-1. 初始化项目结构。
-2. 优先攻克 Spotify 每日 Top 200 的数据持久化(通过爬虫或自动化工具)。
-3. 建立第一个周榜计算模型,并在本周五(3月6日结算日)进行实战对账。
-
----
-
-_Document created for independent project initialization on 2026-03-04_
diff --git a/src/data_collection/city_registry.py b/src/data_collection/city_registry.py
new file mode 100644
index 00000000..8d86c9a0
--- /dev/null
+++ b/src/data_collection/city_registry.py
@@ -0,0 +1,241 @@
+# PolyWeather City Registry
+# A unified "Source of Truth" for all tracked cities, coordinates, ICAO codes, and risk profiles.
+
+CITY_REGISTRY = {
+ "ankara": {
+ "name": "Ankara",
+ "lat": 40.1281,
+ "lon": 32.9951,
+ "icao": "LTAC",
+ "tz_offset": 10800,
+ "use_fahrenheit": False,
+ "risk_level": "medium",
+ "risk_emoji": "🟡",
+ "airport_name": "Esenboğa 机场",
+ "distance_km": 24.5,
+ "warning": "内陆高原城市,昼夜温差大(可达15°C+); 激进取整效应明显。",
+ },
+ "london": {
+ "name": "London",
+ "lat": 51.5048,
+ "lon": 0.0522,
+ "icao": "EGLC",
+ "tz_offset": 0,
+ "use_fahrenheit": False,
+ "risk_level": "low",
+ "risk_emoji": "🟢",
+ "airport_name": "London City 机场",
+ "distance_km": 12.7,
+ "warning": "泰晤士河局部微气候影响。",
+ },
+ "paris": {
+ "name": "Paris",
+ "lat": 49.0097,
+ "lon": 2.5480,
+ "icao": "LFPG",
+ "tz_offset": 3600,
+ "use_fahrenheit": False,
+ "risk_level": "medium",
+ "risk_emoji": "🟡",
+ "airport_name": "Charles de Gaulle 机场",
+ "distance_km": 25.2,
+ "warning": "城市热岛效应:市区比机场偏暖1-2°C。",
+ },
+ "seoul": {
+ "name": "Seoul",
+ "lat": 37.4602,
+ "lon": 126.4407,
+ "icao": "RKSI",
+ "tz_offset": 32400,
+ "use_fahrenheit": False,
+ "risk_level": "high",
+ "risk_emoji": "🔴",
+ "airport_name": "仁川国际机场",
+ "distance_km": 48.8,
+ "warning": "距离太远,海洋性vs大陆性气候差异大。",
+ },
+ "toronto": {
+ "name": "Toronto",
+ "lat": 43.6777,
+ "lon": -79.6248,
+ "icao": "CYYZ",
+ "tz_offset": -18000,
+ "use_fahrenheit": False,
+ "risk_level": "low",
+ "risk_emoji": "🟢",
+ "airport_name": "Pearson 国际机场",
+ "distance_km": 19.6,
+ "warning": "冬季湖效应偶尔导致局部强降温。",
+ },
+ "buenos aires": {
+ "name": "Buenos Aires",
+ "lat": -34.8222,
+ "lon": -58.5358,
+ "icao": "SAEZ",
+ "tz_offset": -10800,
+ "use_fahrenheit": False,
+ "risk_level": "medium",
+ "risk_emoji": "🟡",
+ "airport_name": "Ezeiza 国际机场",
+ "distance_km": 28.1,
+ "warning": "夏天城区可比郊区高2-3°C。",
+ },
+ "wellington": {
+ "name": "Wellington",
+ "lat": -41.3272,
+ "lon": 174.8053,
+ "icao": "NZWN",
+ "tz_offset": 46800,
+ "use_fahrenheit": False,
+ "risk_level": "low",
+ "risk_emoji": "🟢",
+ "airport_name": "惠灵顿国际机场",
+ "distance_km": 5.5,
+ "warning": "风大影响体感,但温度测量偏差小。",
+ },
+ "new york": {
+ "name": "New York",
+ "lat": 40.7769,
+ "lon": -73.8740,
+ "icao": "KLGA",
+ "tz_offset": -18000,
+ "use_fahrenheit": True,
+ "risk_level": "low",
+ "risk_emoji": "🟢",
+ "airport_name": "LaGuardia 机场",
+ "distance_km": 14.5,
+ "warning": "东河水汽可能在春季产生温差。",
+ },
+ "chicago": {
+ "name": "Chicago",
+ "lat": 41.9742,
+ "lon": -87.9073,
+ "icao": "KORD",
+ "tz_offset": -21600,
+ "use_fahrenheit": True,
+ "risk_level": "high",
+ "risk_emoji": "🔴",
+ "airport_name": "O'Hare 国际机场",
+ "distance_km": 25.3,
+ "warning": "密歇根湖效应:湖边vs内陆可差10°F+。",
+ },
+ "dallas": {
+ "name": "Dallas",
+ "lat": 32.8471,
+ "lon": -96.8518,
+ "icao": "KDAL",
+ "tz_offset": -21600,
+ "use_fahrenheit": True,
+ "risk_level": "medium",
+ "risk_emoji": "🟡",
+ "airport_name": "Dallas Love Field 机场",
+ "distance_km": 11.2,
+ "warning": "城市热岛效应在夏季午后会使温度略高于郊区。",
+ },
+ "miami": {
+ "name": "Miami",
+ "lat": 25.7959,
+ "lon": -80.2870,
+ "icao": "KMIA",
+ "tz_offset": -18000,
+ "use_fahrenheit": True,
+ "risk_level": "low",
+ "risk_emoji": "🟢",
+ "airport_name": "Miami 国际机场",
+ "distance_km": 10.3,
+ "warning": "温差较小,数据稳定。",
+ },
+ "atlanta": {
+ "name": "Atlanta",
+ "lat": 33.6407,
+ "lon": -84.4277,
+ "icao": "KATL",
+ "tz_offset": -18000,
+ "use_fahrenheit": True,
+ "risk_level": "low",
+ "risk_emoji": "🟢",
+ "airport_name": "Hartsfield-Jackson 机场",
+ "distance_km": 12.6,
+ "warning": "数据较准。",
+ },
+ "seattle": {
+ "name": "Seattle",
+ "lat": 47.4502,
+ "lon": -122.3088,
+ "icao": "KSEA",
+ "tz_offset": -28800,
+ "use_fahrenheit": True,
+ "risk_level": "low",
+ "risk_emoji": "🟢",
+ "airport_name": "Sea-Tac 国际机场",
+ "distance_km": 17.4,
+ "warning": "微气候差异存在但较小。",
+ },
+ "lucknow": {
+ "name": "Lucknow",
+ "lat": 26.7606,
+ "lon": 80.8893,
+ "icao": "VILK",
+ "tz_offset": 19800,
+ "use_fahrenheit": False,
+ "risk_level": "medium",
+ "risk_emoji": "🟡",
+ "airport_name": "Chaudhary Charan Singh 国际机场",
+ "distance_km": 14.0,
+ "warning": "印度北方热岛效应,夏季午后机场反馈略低于市区。",
+ },
+ "sao paulo": {
+ "name": "São Paulo",
+ "lat": -23.4356,
+ "lon": -46.4731,
+ "icao": "SBGR",
+ "tz_offset": -10800,
+ "use_fahrenheit": False,
+ "risk_level": "high",
+ "risk_emoji": "🔴",
+ "airport_name": "São Paulo/Guarulhos 机场",
+ "distance_km": 25.0,
+ "warning": "距离远且海拔有差异,局部降雨温差极大。",
+ },
+ "munich": {
+ "name": "Munich",
+ "lat": 48.3538,
+ "lon": 11.7861,
+ "icao": "EDDM",
+ "tz_offset": 3600,
+ "use_fahrenheit": False,
+ "risk_level": "high",
+ "risk_emoji": "🔴",
+ "airport_name": "Munich 机场",
+ "distance_km": 28.5,
+ "warning": "距离非常远,空旷机场夜间降温快,冬季易生大雾压制气温。",
+ },
+}
+
+ALIASES = {
+ # English shortcuts
+ "ank": "ankara", "lon": "london", "par": "paris",
+ "nyc": "new york", "ny": "new york", "chi": "chicago",
+ "dal": "dallas", "mia": "miami", "atl": "atlanta",
+ "sea": "seattle", "tor": "toronto", "sel": "seoul",
+ "seo": "seoul", "ba": "buenos aires", "wel": "wellington",
+ "luc": "lucknow", "sp": "sao paulo", "mun": "munich",
+
+ # Chinese names
+ "安卡拉": "ankara",
+ "伦敦": "london",
+ "巴黎": "paris",
+ "纽约": "new york",
+ "芝加哥": "chicago",
+ "达拉斯": "dallas",
+ "迈阿密": "miami",
+ "亚特兰大": "atlanta",
+ "西雅图": "seattle",
+ "多伦多": "toronto",
+ "首尔": "seoul",
+ "布宜诺斯艾利斯": "buenos aires",
+ "惠灵顿": "wellington",
+ "勒克瑙": "lucknow",
+ "圣保罗": "sao paulo",
+ "慕尼黑": "munich",
+}
diff --git a/src/data_collection/city_risk_profiles.py b/src/data_collection/city_risk_profiles.py
index 3ee6bf7a..7e669970 100644
--- a/src/data_collection/city_risk_profiles.py
+++ b/src/data_collection/city_risk_profiles.py
@@ -1,171 +1,28 @@
# Polymarket 城市温度市场 - 数据偏差风险档案
# 基于 METAR 机场站与市区实际温度的系统性差异
+from src.data_collection.city_registry import CITY_REGISTRY
+
+# Generate profiles from registry
CITY_RISK_PROFILES = {
- # 🔴 高危城市 - 数据偏差大,容易误判
- "seoul": {
- "risk_level": "high",
- "risk_emoji": "🔴",
- "icao": "RKSI",
- "airport_name": "仁川国际机场",
- "distance_km": 48.8,
- "elevation_diff_m": 0,
- "typical_bias_f": 5.8,
- "bias_direction": "机场靠海偏暖,市区内陆更冷",
- "warning": "距离太远,根本不是同一个天气区",
- "season_notes": None,
- },
- "chicago": {
- "risk_level": "high",
- "risk_emoji": "🔴",
- "icao": "KORD",
- "airport_name": "O'Hare 国际机场",
- "distance_km": 25.3,
- "elevation_diff_m": 42,
- "typical_bias_f": 4.0,
- "bias_direction": "密歇根湖效应:风向变化时湖边vs内陆可差10°F+",
- "warning": "冬天温差最不稳定",
- "season_notes": "冬季",
- },
- # 🟡 中危城市 - 存在系统偏差,需注意
- "ankara": {
- "risk_level": "medium",
- "risk_emoji": "🟡",
- "icao": "LTAC",
- "airport_name": "Esenboğa 机场",
- "distance_km": 24.5,
- "elevation_diff_m": 65,
- "typical_bias_f": 2.0,
- "bias_direction": "机场海拔更高",
- "warning": "内陆高原城市,昼夜温差大(可达15°C+)",
- "season_notes": "下午最高温时偏差会放大",
- "metar_rounding": "激进取整:METAR 报告的温度偏高,例如实际 3.4°C 可能报告为 4°C,这意味着 METAR 显示的整数温度往往已接近下一个 WU 结算值。",
- },
- "london": {
- "risk_level": "low",
- "risk_emoji": "🟢",
- "icao": "EGLC",
- "airport_name": "London City 机场",
- "distance_km": 12.7,
- "elevation_diff_m": 4,
- "typical_bias_f": 0.5,
- "bias_direction": "河水调节效应:泰晤士河 Royal Docks 使得夏天偏凉,冬天偏暖",
- "warning": "极端天气日(热浪/寒潮)偏差会显著放大",
- "season_notes": None,
- },
- "dallas": {
- "risk_level": "medium",
- "risk_emoji": "🟡",
- "icao": "KDAL",
- "airport_name": "Dallas Love Field 机场",
- "distance_km": 11.2,
- "elevation_diff_m": 0,
- "typical_bias_f": 1.1,
- "bias_direction": "比 DFW 更接近市中心,数据更准",
- "warning": "城市热岛效应在夏季午后会使温度略高于郊区",
- "season_notes": None,
- },
- "buenos aires": {
- "risk_level": "medium",
- "risk_emoji": "🟡",
- "icao": "SAEZ",
- "airport_name": "Ezeiza 国际机场",
- "distance_km": 28.1,
- "elevation_diff_m": 0,
- "typical_bias_f": 1.2,
- "bias_direction": "夏天城区可比郊区高2-3°C",
- "warning": "距离远但地形平坦,偏差稳定可预测",
- "season_notes": "夏季",
- },
- "paris": {
- "risk_level": "medium",
- "risk_emoji": "🟡",
- "icao": "LFPG",
- "airport_name": "Charles de Gaulle 机场",
- "distance_km": 25.2,
- "elevation_diff_m": 26,
- "typical_bias_f": 1.5,
- "bias_direction": "城市热岛效应:市区比机场偏暖1-2°C",
- "warning": "机场在北郊,冬季北风时比市区更冷",
- "season_notes": "夏季热浪期间偏差最大",
- },
- # 🟢 低危城市 - 数据相对靠谱
- "toronto": {
- "risk_level": "low",
- "risk_emoji": "🟢",
- "icao": "CYYZ",
- "airport_name": "Pearson 国际机场",
- "distance_km": 19.6,
- "elevation_diff_m": 0,
- "typical_bias_f": 0.3,
- "bias_direction": None,
- "warning": "冬季湖效应偶尔炸裂",
- "season_notes": "冬季",
- },
- "new york": {
- "risk_level": "low",
- "risk_emoji": "🟢",
- "icao": "KLGA",
- "airport_name": "LaGuardia 机场",
- "distance_km": 14.5,
- "elevation_diff_m": 0,
- "typical_bias_f": 0.7,
- "bias_direction": "相比 JFK 更靠近曼哈顿",
- "warning": "东河水汽可能在春季产生微小的降温效果",
- "season_notes": None,
- },
- "seattle": {
- "risk_level": "low",
- "risk_emoji": "🟢",
- "icao": "KSEA",
- "airport_name": "Sea-Tac 国际机场",
- "distance_km": 17.4,
- "elevation_diff_m": 0,
- "typical_bias_f": 0.6,
- "bias_direction": "微气候差异存在但较小",
- "warning": None,
- "season_notes": None,
- },
- "atlanta": {
- "risk_level": "low",
- "risk_emoji": "🟢",
- "icao": "KATL",
- "airport_name": "Hartsfield-Jackson 机场",
- "distance_km": 12.6,
- "elevation_diff_m": 0,
- "typical_bias_f": 0.5,
- "bias_direction": None,
- "warning": None,
- "season_notes": None,
- },
- "miami": {
- "risk_level": "low",
- "risk_emoji": "🟢",
- "icao": "KMIA",
- "airport_name": "Miami 国际机场",
- "distance_km": 10.3,
- "elevation_diff_m": 0,
- "typical_bias_f": 0.3,
- "bias_direction": None,
- "warning": None,
- "season_notes": None,
- },
- "wellington": {
- "risk_level": "low",
- "risk_emoji": "🟢",
- "icao": "NZWN",
- "airport_name": "Wellington 机场",
- "distance_km": 5.1,
- "elevation_diff_m": 0,
- "typical_bias_f": 0.2,
- "bias_direction": None,
- "warning": "12城最近,数据最靠谱",
- "season_notes": None,
- },
+ cid: {
+ "risk_level": info["risk_level"],
+ "risk_emoji": info["risk_emoji"],
+ "icao": info["icao"],
+ "airport_name": info["airport_name"],
+ "distance_km": info["distance_km"],
+ "warning": info["warning"],
+ # Backwards compatibility flags if needed
+ "typical_bias_f": info.get("typical_bias_f", 0.0),
+ "elevation_diff_m": info.get("elevation_diff_m", 0),
+ "bias_direction": info.get("bias_direction", None),
+ "season_notes": info.get("season_notes", None),
+ }
+ for cid, info in CITY_REGISTRY.items()
}
-def get_city_risk_profile(city_name: str) -> dict:
+def get_city_risk_profile(city: str) -> dict:
"""获取城市的风险档案"""
city_lower = city_name.lower().strip()
diff --git a/src/data_collection/weather_sources.py b/src/data_collection/weather_sources.py
index d98cec15..4d7c4312 100644
--- a/src/data_collection/weather_sources.py
+++ b/src/data_collection/weather_sources.py
@@ -17,24 +17,10 @@ class WeatherDataCollector:
- NOAA Aviation Weather (METAR - airport observations)
"""
- # Polymarket 12 个天气市场对应的 ICAO 机场代码
- # 这些是 Weather Underground 结算源使用的气象站
- CITY_TO_ICAO = {
- "seattle": "KSEA", # Seattle-Tacoma Airport
- "london": "EGLC", # London City Airport
- "dallas": "KDAL", # Dallas Love Field
- "miami": "KMIA", # Miami International
- "atlanta": "KATL", # Hartsfield-Jackson
- "chicago": "KORD", # O'Hare International
- "new york": "KLGA", # LaGuardia Airport
- "nyc": "KLGA", # Alias
- "seoul": "RKSI", # Incheon International
- "ankara": "LTAC", # Esenboğa International
- "toronto": "CYYZ", # Toronto Pearson
- "wellington": "NZWN", # Wellington International
- "buenos aires": "SAEZ", # Ezeiza International
- "paris": "LFPG", # Charles de Gaulle
- }
+ from src.data_collection.city_registry import CITY_REGISTRY
+ CITY_TO_ICAO = {cid: info["icao"] for cid, info in CITY_REGISTRY.items()}
+ # Alias
+ CITY_TO_ICAO["nyc"] = "KLGA"
# 城市周边 METAR 集群(用于在全球城市模拟类似安卡拉的多测站地图分布)
CITY_METAR_CLUSTERS = {
@@ -49,6 +35,8 @@ class WeatherDataCollector:
"atlanta": ["KATL", "KPDK", "KFTY"],
"miami": ["KMIA", "KOPF", "KTMB"],
"seattle": ["KSEA", "KBFI", "KPAE"],
+ "sao paulo": ["SBGR", "SBSP", "SBKP"],
+ "munich": ["EDDM", "EDMO", "EDJA"],
}
def __init__(self, config: dict):
@@ -1196,34 +1184,29 @@ class WeatherDataCollector:
"""
使用 Open-Meteo Geocoding API 获取城市坐标 (免费, 无需 Key)
"""
- # 坐标使用 METAR 机场位置(Polymarket 以机场数据结算)
- static_coords = {
- "london": {"lat": 51.5053, "lon": 0.0553}, # EGLC London City
- "paris": {"lat": 49.0097, "lon": 2.5478}, # LFPG Charles de Gaulle
- "new york": {"lat": 40.7750, "lon": -73.8750}, # KLGA LaGuardia
- "new york's central park": {"lat": 40.7812, "lon": -73.9665},
- "nyc": {"lat": 40.7750, "lon": -73.8750}, # KLGA LaGuardia
- "seattle": {"lat": 47.4499, "lon": -122.3118}, # KSEA Sea-Tac
- "chicago": {"lat": 41.9769, "lon": -87.9081}, # KORD O'Hare
- "dallas": {"lat": 32.8459, "lon": -96.8509}, # KDAL Love Field
- "miami": {"lat": 25.7933, "lon": -80.2906}, # KMIA International
- "atlanta": {"lat": 33.6367, "lon": -84.4281}, # KATL Hartsfield-Jackson
- "seoul": {"lat": 37.4691, "lon": 126.4510}, # RKSI Incheon
- "toronto": {"lat": 43.6759, "lon": -79.6294}, # CYYZ Pearson
- "ankara": {"lat": 40.1281, "lon": 32.9950}, # LTAC Esenboğa
- "wellington": {"lat": -41.3272, "lon": 174.8053}, # NZWN Wellington
- "buenos aires": {"lat": -34.8222, "lon": -58.5358}, # SAEZ Ezeiza
- }
-
+ from src.data_collection.city_registry import CITY_REGISTRY
normalized_city = city.lower().strip()
- if normalized_city in static_coords:
- return static_coords[normalized_city]
- # 模糊匹配映射 (针对包含城市名的情况)
- for key in static_coords:
+ # 1. Check registry first (Source of Truth)
+ if normalized_city in CITY_REGISTRY:
+ info = CITY_REGISTRY[normalized_city]
+ return {"lat": info["lat"], "lon": info["lon"]}
+
+ # 2. Hardcoded specific cases or aliases
+ static_aliases = {
+ "new york's central park": "new york",
+ "nyc": "new york"
+ }
+ if normalized_city in static_aliases:
+ root_city = static_aliases[normalized_city]
+ info = CITY_REGISTRY[root_city]
+ return {"lat": info["lat"], "lon": info["lon"]}
+
+ for key in CITY_REGISTRY:
if key in normalized_city:
logger.debug(f"地理编码命中模糊映射: {city} -> {key}")
- return static_coords[key]
+ info = CITY_REGISTRY[key]
+ return {"lat": info["lat"], "lon": info["lon"]}
try:
url = "https://geocoding-api.open-meteo.com/v1/search"
diff --git a/web/app.py b/web/app.py
index 64bb644a..1a66da78 100644
--- a/web/app.py
+++ b/web/app.py
@@ -39,31 +39,20 @@ app.mount("/static", StaticFiles(directory=_static), name="static")
_config = load_config()
_weather = WeatherDataCollector(_config)
-# ──────────────────────────────────────────────────────────
-# City Registry
-# ──────────────────────────────────────────────────────────
-CITIES: Dict[str, Dict[str, Any]] = {
- "ankara": {"lat": 40.1281, "lon": 32.9951, "f": False, "tz": 10800},
- "london": {"lat": 51.5048, "lon": 0.0522, "f": False, "tz": 0},
- "paris": {"lat": 49.0097, "lon": 2.5480, "f": False, "tz": 3600},
- "seoul": {"lat": 37.4602, "lon": 126.4407, "f": False, "tz": 32400},
- "toronto": {"lat": 43.6777, "lon": -79.6248, "f": False, "tz": -18000},
- "buenos aires": {"lat": -34.8222, "lon": -58.5358, "f": False, "tz": -10800},
- "wellington": {"lat": -41.3272, "lon": 174.8053, "f": False, "tz": 46800},
- "new york": {"lat": 40.7769, "lon": -73.8740, "f": True, "tz": -18000},
- "chicago": {"lat": 41.9742, "lon": -87.9073, "f": True, "tz": -21600},
- "dallas": {"lat": 32.8471, "lon": -96.8518, "f": True, "tz": -21600},
- "miami": {"lat": 25.7959, "lon": -80.2870, "f": True, "tz": -18000},
- "atlanta": {"lat": 33.6407, "lon": -84.4277, "f": True, "tz": -18000},
- "seattle": {"lat": 47.4502, "lon": -122.3088, "f": True, "tz": -28800},
-}
+from src.data_collection.city_registry import CITY_REGISTRY, ALIASES
-ALIASES = {
- "ank": "ankara", "lon": "london", "par": "paris",
- "nyc": "new york", "chi": "chicago", "dal": "dallas",
- "mia": "miami", "atl": "atlanta", "sea": "seattle",
- "tor": "toronto", "sel": "seoul", "ba": "buenos aires",
- "wel": "wellington",
+# ──────────────────────────────────────────────────────────
+# City Registry Transformation
+# ──────────────────────────────────────────────────────────
+# Convert registry to the internal format expected by app logic
+CITIES: Dict[str, Dict[str, Any]] = {
+ cid: {
+ "lat": info["lat"],
+ "lon": info["lon"],
+ "f": info["use_fahrenheit"],
+ "tz": info["tz_offset"]
+ }
+ for cid, info in CITY_REGISTRY.items()
}
# ──────────────────────────────────────────────────────────