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() } # ──────────────────────────────────────────────────────────