feat: Integrate multi-model and ensemble weather forecasts, update READMEs with new architecture and betting strategies, and resolve merge conflicts.

This commit is contained in:
2569718930@qq.com
2026-02-23 21:04:44 +08:00
parent b89b137e83
commit be90e610ee
3 changed files with 0 additions and 190 deletions
-28
View File
@@ -8,11 +8,7 @@ An intelligent weather bot for prediction markets and professional weather betti
- **Python 3.11+**
- Dependencies: `pip install -r requirements.txt`
<<<<<<< HEAD
- **Environment**: Configure `METEOBLUE_API_KEY` in `.env` to enable high-precision London forecasts.
=======
- **Environment Variables**: Set `TELEGRAM_BOT_TOKEN` in `.env` (required). Optionally set `METEOBLUE_API_KEY` for London high-precision forecasts.
>>>>>>> e575440acfd8b5f1e8c30e83dfcb972d26175729
### VPS Deployment (Recommended)
@@ -110,17 +106,7 @@ py -3.11 run.py
| **NWS** | Official (US) | US Only | US National Weather Service high-fidelity forecasts |
| **MGM** | Observations (Turkey) | Ankara Only | Turkish State Met Service: pressure, cloud cover, feels-like, 24h rainfall |
<<<<<<< HEAD
| Source | Role | Coverage | Strength |
| :----------------- | :---------------------- | :-------------- | :--------------------------------------------------------------------------------- |
| **Open-Meteo** | Base Forecast | Global | Provides detailed 72-hour temperature curves for all cities. |
| **Meteoblue (MB)** | **Precision Consensus** | London Only | **Traders' choice**. Aggregates multiple models; excellent for microclimates. |
| **METAR** | **Settlement Standard** | Global Airports | The absolute truth for Polymarket settlement; real-time station data. |
| **NWS** | Official (US) | US Only | High-fidelity forecasts for US cities, critical for extreme weather events. |
| **MGM** | Official (Turkey) | Ankara | Direct access to Turkish State Meteorological Service for local official accuracy. |
=======
> ⚠️ **All NWP model queries use airport coordinates** (matching METAR station), not city center. This eliminates systematic bias between forecast and settlement locations.
>>>>>>> e575440acfd8b5f1e8c30e83dfcb972d26175729
**Open-Meteo API Architecture**: Three API calls go through the same platform, each serving a different purpose:
@@ -240,19 +226,12 @@ graph TD
Bot --> Collector[WeatherDataCollector]
subgraph "Data Engine"
<<<<<<< HEAD
Collector --> OM[Open-Meteo API]
Collector --> MB[Meteoblue Weather API]
Collector --> NOAA[METAR Data Center]
Collector --> MGM[Turkish MGM API]
=======
Collector --> MM[Multi-Model API<br/>ECMWF/GFS/ICON/GEM/JMA]
Collector --> OM[Open-Meteo Forecast]
Collector --> ENS[Open-Meteo Ensemble]
Collector --> MB[Meteoblue API]
Collector --> NOAA[METAR / NOAA]
Collector --> MGM[MGM Observations]
>>>>>>> e575440acfd8b5f1e8c30e83dfcb972d26175729
Collector --> NWS[US NWS API]
end
@@ -271,12 +250,6 @@ graph TD
## 🎯 Betting Strategy Tips
<<<<<<< HEAD
1. **Check Consensus**: Compare Open-Meteo and Meteoblue (MB). Consensus usually implies higher probability.
2. **Watch the Peak**: Use `/city` frequently during predicted peak windows to catch momentum.
3. **Weighting Hierarchy**: Settlement is **METAR**; high-accuracy trend is **MB** (London); Official (NWS/MGM) is the "anchor."
4. **Geographic Risk**: Pay close attention to cities where "Bias will significantly amplify."
=======
1. **Check Model Consensus**: The 🎯/⚖️/⚠️ rating tells you immediately if the forecast is reliable.
2. **Use the Entry Signal**: Wait for ⏰ **Ideal** or **Good** timing before placing bets. Don't bet early when uncertainty is high.
3. **Watch Ensemble Spread**: A tight 90% band (< 2°) means model confidence is high — this is where edges live.
@@ -289,4 +262,3 @@ graph TD
---
_Last updated: 2026-02-22_
>>>>>>> e575440acfd8b5f1e8c30e83dfcb972d26175729
-29
View File
@@ -8,11 +8,7 @@
- **Python 3.11+**
- 依赖安装: `pip install -r requirements.txt`
<<<<<<< HEAD
- **环境变量**: 需在 `.env` 中配置 `METEOBLUE_API_KEY` 以激活伦敦高精度预报。
=======
- **环境变量**: 在 `.env` 中设置 `TELEGRAM_BOT_TOKEN`(必需)。可选设置 `METEOBLUE_API_KEY` 以激活伦敦高精度预报。
>>>>>>> e575440acfd8b5f1e8c30e83dfcb972d26175729
### VPS 部署 (推荐)
@@ -110,17 +106,7 @@ py -3.11 run.py
| **NWS** | 官方预测(美) | 仅限美国 | 美国国家气象局高精度预报 |
| **MGM** | 实测数据(土) | 仅限安卡拉 | 土耳其气象局:气压、云量、体感温度、24h 降水 |
<<<<<<< HEAD
| 数据源 | 数据角色 | 覆盖范围 | 优势 |
| :----------------- | :------------- | :--------- | :----------------------------------------------- |
| **Open-Meteo** | 基础预测 | 全球 | 提供所有城市的 72 小时精细化温度曲线 |
| **Meteoblue (MB)** | **高精度共识** | 仅限伦敦 | **交易员首选**。聚合多家模型,对微气候处理极佳 |
| **METAR** | **结算标准** | 全球机场 | Polymarket 结算参考的绝对真理,实时机场观测 |
| **NWS** | 官方预测(美) | 仅限美国 | 美国国家气象局,对美国城市的极端天气预判准确 |
| **MGM** | 官方预测(土) | 仅限安卡拉 | 土耳其气象局,提供安卡拉 Esenboğa 机场的官方数据 |
=======
> ⚠️ **所有 NWP 模型查询使用机场坐标**(与 METAR 站点一致),而非市中心。这消除了预报位置与结算位置之间的系统性偏差。
>>>>>>> e575440acfd8b5f1e8c30e83dfcb972d26175729
**Open-Meteo API 架构关系**:三个 API 调用都经过 Open-Meteo 平台,但获取的是不同维度的数据:
@@ -244,13 +230,6 @@ graph TD
User[/Telegram User/] --> Bot[bot_listener.py]
Bot --> Collector[WeatherDataCollector]
<<<<<<< HEAD
subgraph "Data Engine"
Collector --> OM[Open-Meteo API]
Collector --> MB[Meteoblue Weather API]
Collector --> NOAA[METAR Data Center]
Collector --> MGM[Turkish MGM API]
=======
subgraph "数据引擎"
Collector --> MM[多模型 API<br/>ECMWF/GFS/ICON/GEM/JMA]
Collector --> OM[Open-Meteo 预报]
@@ -258,7 +237,6 @@ graph TD
Collector --> MB[Meteoblue API]
Collector --> NOAA[METAR / NOAA]
Collector --> MGM[MGM 实测数据]
>>>>>>> e575440acfd8b5f1e8c30e83dfcb972d26175729
Collector --> NWS[US NWS API]
end
@@ -277,12 +255,6 @@ graph TD
## 🎯 博弈策略提示
<<<<<<< HEAD
1. **检查模型共识**:查看 Open-Meteo 和 Meteoblue (MB) 是否达成共识。
2. **关注峰值窗口**:在预测的峰值时段多次使用 `/city` 刷新。
3. **数据权重优先级**:结算以 **METAR** 为准,趋势预测以 **MB** 为准(仅限伦敦)。
4. **地理风险评估**:重点关注提示中的“偏差会显著放大”警告(如安卡拉、伦敦)。
=======
1. **看模型共识**:🎯/⚖️/⚠️ 评级让你一眼判断预报是否可靠。高共识 + 市场低定价 = 套利机会。
2. **用入场信号**:等 ⏰ **理想****较好** 时机再下注。不确定性高时绝不提前入场。
3. **关注集合散度**:90% 区间越窄(< 2°),模型置信越高 — 这才是 edge 所在。
@@ -295,4 +267,3 @@ graph TD
---
_最后更新: 2026-02-22_
>>>>>>> e575440acfd8b5f1e8c30e83dfcb972d26175729
-133
View File
@@ -37,29 +37,21 @@ def analyze_weather_trend(weather_data, temp_symbol):
forecast_highs.append(mb["today_high"])
if nws.get("today_high") is not None:
forecast_highs.append(nws["today_high"])
<<<<<<< HEAD
if mgm.get("today_high") is not None:
forecast_highs.append(mgm["today_high"])
=======
# 加入多模型预报 (ECMWF, GFS, ICON, GEM, JMA)
for mv in weather_data.get("multi_model", {}).get("forecasts", {}).values():
if mv is not None:
forecast_highs.append(mv)
>>>>>>> e575440acfd8b5f1e8c30e83dfcb972d26175729
forecast_highs = [h for h in forecast_highs if h is not None]
# 取预报中的最高值作为风险防御基准
forecast_high = max(forecast_highs) if forecast_highs else None
# 取最低值用于判断是否“已触及预报高位”
min_forecast_high = min(forecast_highs) if forecast_highs else forecast_high
<<<<<<< HEAD
=======
# 取中位数作为用户可见的"预期值"(避免极端模型误导)
forecast_median = None
if forecast_highs:
sorted_fh = sorted(forecast_highs)
forecast_median = sorted_fh[len(sorted_fh) // 2]
>>>>>>> e575440acfd8b5f1e8c30e83dfcb972d26175729
wind_speed = metar.get("current", {}).get("wind_speed_kt", 0)
@@ -186,14 +178,6 @@ def analyze_weather_trend(weather_data, temp_symbol):
is_breakthrough = False
if max_so_far is not None and forecast_high is not None:
if max_so_far > forecast_high + 0.5:
<<<<<<< HEAD
# 实测已超所有预报!
exceed_by = max_so_far - forecast_high
insights.append(f"🚨 <b>预报已被击穿</b>:实测最高 {max_so_far}{temp_symbol} 已超所有预报上限 {forecast_high}{temp_symbol}{exceed_by:.1f}°!")
insights.append(f"💡 <b>博弈建议</b>:市场需重新评估,当前可能存在极端异常增温。")
return "\n💡 <b>态势分析</b>\n" + "\n".join(insights)
=======
is_breakthrough = True
exceed_by = max_so_far - forecast_high
insights.append(f"🚨 <b>实测已超预报</b>:实测最高 {max_so_far}{temp_symbol} 超过了所有预报的天花板 {forecast_high}{temp_symbol},多了 {exceed_by:.1f}°!")
@@ -221,7 +205,6 @@ def analyze_weather_trend(weather_data, temp_symbol):
f"刚刚越过进位线,再降 <b>{fractional - 0.5:.1f}°</b> 就会回落到 {settled - 1}{temp_symbol}"
)
>>>>>>> e575440acfd8b5f1e8c30e83dfcb972d26175729
# --- 峰值时刻预测逻辑 (仍以 Open-Meteo 逐小时数据为准) ---
hourly = open_meteo.get("hourly", {})
times = hourly.get("time", [])
@@ -236,48 +219,6 @@ def analyze_weather_trend(weather_data, temp_symbol):
hour = t_str.split("T")[1][:5]
peak_hours.append(hour)
<<<<<<< HEAD
if peak_hours:
window = f"{peak_hours[0]} - {peak_hours[-1]}" if len(peak_hours) > 1 else peak_hours[0]
insights.append(f"⏱️ <b>预计峰值时刻</b>:今天 <b>{window}</b> 之间。")
# 只有在还没进入峰值时段且还没达到预报高点时才给这个建议
if local_hour < int(peak_hours[0].split(":")[0]) and (max_so_far is None or max_so_far < forecast_high):
insights.append(f"🎯 <b>博弈建议</b>:关注该时段实测能否站稳 {forecast_high}{temp_symbol}")
is_peak_passed = False
if curr_temp is not None and forecast_high is not None:
diff_max = forecast_high - curr_temp
# 1. 气温节奏判定 (动态参考峰值时刻)
last_peak_h = int(peak_hours[-1].split(":")[0]) if peak_hours else 15
first_peak_h = int(peak_hours[0].split(":")[0]) if peak_hours else 13
if local_hour > last_peak_h:
# 已经过了预报的峰值时段
is_peak_passed = True
# 如果实测已经接近“任一”主流预报的最高温 (使用 min_forecast_high)
if max_so_far and max_so_far >= min_forecast_high - 0.5:
insights.append(f"✅ <b>今日峰值已过</b>:气温已触及或接近预报最高,目前处于高位波动或缓慢回落。")
else:
# 虽然时间过了,但离最高温还有差距
insights.append(f"📉 <b>处于降温期</b>:已过预报峰值时段,且当前气温乏力 ({curr_temp}{temp_symbol}),冲击最高预报 {forecast_high}{temp_symbol} 的概率降低。")
elif first_peak_h <= local_hour <= last_peak_h:
# 正在峰值窗口内
if diff_max <= 0.8:
insights.append(f"⚖️ <b>高位横盘</b>:正处于预测峰值时段,气温将在当前水平小幅波动。")
else:
insights.append(f"⏳ <b>峰值窗口中</b>:虽在预报高点时段,但目前仍有差距,紧盯最后冲刺。")
elif local_hour < first_peak_h:
# 还没到峰值窗口
if diff_max > 1.2:
insights.append(f"📈 <b>升温进程中</b>:距离峰值时段还有 {first_peak_h - local_hour}h,正向高点冲击。")
else:
insights.append(f"🌅 <b>临近峰值</b>:即将进入高点时段,气温已处于预报高位。")
else:
# 回退逻辑
insights.append(f"🌌 <b>夜间/早间</b>:等待日出后的新一轮波动。")
=======
# 确定用于逻辑判断的峰值小时
if peak_hours:
first_peak_h = int(peak_hours[0].split(":")[0])
@@ -331,43 +272,20 @@ def analyze_weather_trend(weather_data, temp_symbol):
# 回退逻辑
insights.append(f"🌌 <b>夜间</b>:等明天太阳出来后再看新一轮升温。")
>>>>>>> e575440acfd8b5f1e8c30e83dfcb972d26175729
# 2. 湿度与露点分析 (仅在傍晚以后)
humidity = metar.get("current", {}).get("humidity")
dewpoint = metar.get("current", {}).get("dewpoint")
if local_hour >= 18:
if humidity and humidity > 80:
<<<<<<< HEAD
insights.append(f"💦 <b>闷热高湿</b>:湿度极高 ({humidity}%),将显著锁住夜间热量。")
if dewpoint is not None and curr_temp - dewpoint < 2.0:
insights.append(f"🌡️ <b>触及露点支撑</b>:气温已跌至露点支撑位,降温将变慢。")
=======
insights.append(f"💦 <b>湿度很高</b>:湿度 {humidity}%,空气很潮湿,夜里热量散不掉,降温会很慢。")
if dewpoint is not None and curr_temp - dewpoint < 2.0:
insights.append(f"🌡️ <b>降温快到底了</b>:温度已经接近露点(空气中水汽开始凝结的温度),再往下降会很困难。")
>>>>>>> e575440acfd8b5f1e8c30e83dfcb972d26175729
# 3. 风力
if wind_speed >= 15:
insights.append(f"🌬️ <b>风很大</b>:风速 {wind_speed}kt,温度可能会忽高忽低。")
elif wind_speed >= 10:
<<<<<<< HEAD
insights.append(f"🍃 <b>清劲风</b>:空气流动快,虽然有助于散热,但在升温期可能带来暖平流加速。")
# 4. 云层遮挡分析 (仅在升温期/峰值期有意义)
clouds = metar.get("current", {}).get("clouds", [])
if clouds and local_hour <= last_peak_h + 1:
main_cloud = clouds[-1]
cover = main_cloud.get("cover", "")
if cover == "OVC":
insights.append(f"☁️ <b>全阴锁温</b>:机场上空完全遮挡,阳光增温几乎停滞,很难再冲高点。")
elif cover == "BKN":
insights.append(f"🌥️ <b>云层显著</b>:天空大部被遮挡,日照受限,升温斜率受阻。")
elif cover in ["SKC", "CLR", "FEW"]:
if not is_peak_passed:
insights.append(f"☀️ <b>晴空万里</b>:日照强烈,无云层遮挡,气温有冲向预报上限甚至超出的动能。")
=======
insights.append(f"🍃 <b>有风</b>:风速适中 ({wind_speed}kt),会加速空气流动,具体影响看风向。")
# 4. 云层遮挡分析 (仅在升温期/峰值期有意义)
@@ -381,38 +299,12 @@ def analyze_weather_trend(weather_data, temp_symbol):
insights.append(f"🌥️ <b>云比较多</b>:天空大部分被云挡住了,日照不足,升温会比较慢。")
elif cover in ["SKC", "CLR", "FEW"]:
insights.append(f"☀️ <b>大晴天</b>:阳光直射,没什么云,有利于温度继续往上冲。")
>>>>>>> e575440acfd8b5f1e8c30e83dfcb972d26175729
# 5. 特殊天气现象
wx_desc = metar.get("current", {}).get("wx_desc")
has_mgm = bool(mgm.get("current"))
mgm_rain = mgm.get("current", {}).get("rain_24h")
if wx_desc:
<<<<<<< HEAD
if any(x in wx_desc.upper() for x in ["RA", "DZ", "RAIN", "DRIZZLE"]):
insights.append(f"🌧️ <b>降雨压制</b>:当前有降雨,蒸发吸热将显著抑制升温。")
elif any(x in wx_desc.upper() for x in ["SN", "SNOW", "GR", "GS"]):
insights.append(f"❄️ <b>固态降水</b>:正在降雪或冰雹,气温将持续低迷。")
elif any(x in wx_desc.upper() for x in ["FG", "BR", "HZ", "FOG", "MIST"]):
insights.append(f"🌫️ <b>能见度受限</b>:当前有雾/霭,阻挡阳光并带来高湿,会大幅延缓升温周期。")
# 6. 风向平流分析 (仅在未进入降温期前显示)
if not is_peak_passed or local_hour <= last_peak_h + 2:
try:
wind_dir = float(metar.get("current", {}).get("wind_dir", 0))
# 北半球简化逻辑:北风 cold,南风 warm
if 315 <= wind_dir or wind_dir <= 45:
insights.append(f"🌬️ <b>偏北风</b>:冷空气处于主导地位,午后增温阻力较大。")
elif 135 <= wind_dir <= 225:
# 只有在当前温度离最高预测还有距离时,南风才有意义
if diff_max > 0.5:
if is_peak_passed:
insights.append(f"🔥 <b>偏南风</b>:存在暖平流支撑,但已过传统峰值时段,冲击上限 {forecast_high}{temp_symbol} 的动能正在衰减。")
else:
insights.append(f"🔥 <b>偏南风</b>:正从低纬度输送暖平流,气温仍有向上突围的潜力。")
except (TypeError, ValueError):
pass
=======
wx_upper = wx_desc.upper().strip()
wx_tokens = wx_upper.split()
# 用分词匹配,避免 "METAR" 中的 "RA" 误判
@@ -483,7 +375,6 @@ def analyze_weather_trend(weather_data, temp_symbol):
insights.append(f"🌬️ <b>吹东风</b>{wind_source} {wd:.0f}°):对温度影响较小,主要看日照和云量。")
except (TypeError, ValueError):
pass
>>>>>>> e575440acfd8b5f1e8c30e83dfcb972d26175729
try:
visibility = metar.get("current", {}).get("visibility_mi")
@@ -494,16 +385,6 @@ def analyze_weather_trend(weather_data, temp_symbol):
except (TypeError, ValueError):
pass
<<<<<<< HEAD
# 7. 模型准确度预警 (针对用户反馈的 MB 偏高问题)
if is_peak_passed and max_so_far is not None:
model_checks = []
if om_high and om_high > max_so_far + 1.5:
model_checks.append(f"Open-Meteo ({om_high}{temp_symbol})")
mb_h = mb.get("today_high")
if mb_h and mb_h > max_so_far + 1.5:
model_checks.append(f"Meteoblue ({mb_h}{temp_symbol})")
=======
# 7. 模型准确度预警(使用多模型数据)
if is_peak_passed and max_so_far is not None:
model_checks = []
@@ -514,16 +395,11 @@ def analyze_weather_trend(weather_data, temp_symbol):
mb_h = mb.get("today_high")
if mb_h and mb_h > max_so_far + 1.5:
model_checks.append(f"MB ({mb_h}{temp_symbol})")
>>>>>>> e575440acfd8b5f1e8c30e83dfcb972d26175729
nws_h = nws.get("today_high")
if nws_h and nws_h > max_so_far + 1.5:
model_checks.append(f"NWS ({nws_h}{temp_symbol})")
if model_checks:
<<<<<<< HEAD
insights.append(f"⚠️ <b>预报偏高</b>:目前实测远低于 " + "".join(model_checks) + ",判定预报模型今日表现过度乐观。")
=======
insights.append(f"⚠️ <b>预报偏高了</b>:实测远低于 " + "".join(model_checks) + ",这些模型今天报高了。")
# 8. MGM 气压分析 (仅安卡拉)
@@ -638,7 +514,6 @@ def analyze_weather_trend(weather_data, temp_symbol):
insights.append(f"⏰ <b>入场时机:谨慎</b> — {factors_str}。建议继续观察。")
else:
insights.append(f"⏰ <b>入场时机:不建议</b> — {factors_str}。不确定性大,等更多数据。")
>>>>>>> e575440acfd8b5f1e8c30e83dfcb972d26175729
if not insights:
return ""
@@ -708,13 +583,6 @@ def start_bot():
"par": "paris", "巴黎": "paris",
}
<<<<<<< HEAD
# 1. 第一优先级:严格全字匹配
city_name = STANDARD_MAPPING.get(city_input)
# 2. 第二优先级:如果长度 >= 3,尝试前缀匹配
if not city_name and len(city_input) >= 3:
=======
# 支持的城市全名列表(用于模糊匹配)
SUPPORTED_CITIES = list(set(STANDARD_MAPPING.values()))
@@ -728,7 +596,6 @@ def start_bot():
# 3. 第三优先级:前缀匹配(在别名和城市全名中搜索)
if not city_name and len(city_input) >= 2:
# 先搜别名
>>>>>>> e575440acfd8b5f1e8c30e83dfcb972d26175729
for k, v in STANDARD_MAPPING.items():
if k.startswith(city_input):
city_name = v