fix(ci): format code and fix ruff lintings issues
This commit is contained in:
+393
-142
@@ -1,7 +1,6 @@
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
from datetime import datetime
|
from typing import List
|
||||||
from typing import List, Dict, Any, Optional
|
|
||||||
import telebot # type: ignore
|
import telebot # type: ignore
|
||||||
from loguru import logger # type: ignore
|
from loguru import logger # type: ignore
|
||||||
|
|
||||||
@@ -10,13 +9,14 @@ project_root = os.path.dirname(os.path.abspath(__file__))
|
|||||||
if project_root not in sys.path:
|
if project_root not in sys.path:
|
||||||
sys.path.insert(0, project_root)
|
sys.path.insert(0, project_root)
|
||||||
|
|
||||||
from src.utils.config_loader import load_config # type: ignore
|
from src.utils.config_loader import load_config # type: ignore # noqa: E402
|
||||||
from src.data_collection.weather_sources import WeatherDataCollector # type: ignore
|
from src.data_collection.weather_sources import WeatherDataCollector # type: ignore # noqa: E402
|
||||||
from src.data_collection.city_risk_profiles import get_city_risk_profile, format_risk_warning # type: ignore
|
from src.data_collection.city_risk_profiles import get_city_risk_profile # type: ignore # noqa: E402
|
||||||
from src.analysis.deb_algorithm import calculate_dynamic_weights, update_daily_record
|
from src.analysis.deb_algorithm import calculate_dynamic_weights, update_daily_record # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
def analyze_weather_trend(weather_data, temp_symbol, city_name=None):
|
def analyze_weather_trend(weather_data, temp_symbol, city_name=None):
|
||||||
'''根据实测与预测分析气温态势,增加峰值时刻预测'''
|
"""根据实测与预测分析气温态势,增加峰值时刻预测"""
|
||||||
insights: List[str] = []
|
insights: List[str] = []
|
||||||
ai_features: List[str] = []
|
ai_features: List[str] = []
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ def analyze_weather_trend(weather_data, temp_symbol, city_name=None):
|
|||||||
open_meteo = weather_data.get("open-meteo", {})
|
open_meteo = weather_data.get("open-meteo", {})
|
||||||
mb = weather_data.get("meteoblue", {})
|
mb = weather_data.get("meteoblue", {})
|
||||||
nws = weather_data.get("nws", {})
|
nws = weather_data.get("nws", {})
|
||||||
mgm = weather_data.get("mgm", {})
|
weather_data.get("mgm", {})
|
||||||
|
|
||||||
if not metar or not open_meteo:
|
if not metar or not open_meteo:
|
||||||
return "", ""
|
return "", ""
|
||||||
@@ -32,11 +32,14 @@ def analyze_weather_trend(weather_data, temp_symbol, city_name=None):
|
|||||||
# 数值归一化:防止 JSON 反序列化后的 str 类型炸数学运算
|
# 数值归一化:防止 JSON 反序列化后的 str 类型炸数学运算
|
||||||
def _sf(v):
|
def _sf(v):
|
||||||
"""safe float"""
|
"""safe float"""
|
||||||
if v is None: return None
|
if v is None:
|
||||||
try: return float(v)
|
return None
|
||||||
except: return None
|
try:
|
||||||
|
return float(v)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
curr_temp = _sf(metar.get("current", {}).get("temp"))
|
_sf(metar.get("current", {}).get("temp"))
|
||||||
max_so_far = _sf(metar.get("current", {}).get("max_temp_so_far"))
|
max_so_far = _sf(metar.get("current", {}).get("max_temp_so_far"))
|
||||||
daily = open_meteo.get("daily", {})
|
daily = open_meteo.get("daily", {})
|
||||||
hourly = open_meteo.get("hourly", {})
|
hourly = open_meteo.get("hourly", {})
|
||||||
@@ -61,8 +64,10 @@ def analyze_weather_trend(weather_data, temp_symbol, city_name=None):
|
|||||||
# 从 URL/入参里我们暂时拿不到城名,为了 DEB 追溯我们在后方的总控那里提取。这里的 analyze_weather_trend 主要计算最高预留。
|
# 从 URL/入参里我们暂时拿不到城名,为了 DEB 追溯我们在后方的总控那里提取。这里的 analyze_weather_trend 主要计算最高预留。
|
||||||
forecast_highs = [h for h in current_forecasts.values() if h is not None]
|
forecast_highs = [h for h in current_forecasts.values() if h is not None]
|
||||||
forecast_high = max(forecast_highs) if forecast_highs else None
|
forecast_high = max(forecast_highs) if forecast_highs else None
|
||||||
min_forecast_high = min(forecast_highs) if forecast_highs else forecast_high
|
min(forecast_highs) if forecast_highs else forecast_high
|
||||||
forecast_median = sorted(forecast_highs)[len(forecast_highs) // 2] if forecast_highs else None
|
forecast_median = (
|
||||||
|
sorted(forecast_highs)[len(forecast_highs) // 2] if forecast_highs else None
|
||||||
|
)
|
||||||
|
|
||||||
wind_speed = metar.get("current", {}).get("wind_speed_kt", 0)
|
wind_speed = metar.get("current", {}).get("wind_speed_kt", 0)
|
||||||
|
|
||||||
@@ -73,8 +78,9 @@ def analyze_weather_trend(weather_data, temp_symbol, city_name=None):
|
|||||||
time_parts = local_time_full.split(" ")[1].split(":")
|
time_parts = local_time_full.split(" ")[1].split(":")
|
||||||
local_hour = int(time_parts[0])
|
local_hour = int(time_parts[0])
|
||||||
local_minute = int(time_parts[1]) if len(time_parts) > 1 else 0
|
local_minute = int(time_parts[1]) if len(time_parts) > 1 else 0
|
||||||
except:
|
except Exception:
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
local_date_str = datetime.now().strftime("%Y-%m-%d")
|
local_date_str = datetime.now().strftime("%Y-%m-%d")
|
||||||
local_hour = datetime.now().hour
|
local_hour = datetime.now().hour
|
||||||
local_minute = datetime.now().minute
|
local_minute = datetime.now().minute
|
||||||
@@ -82,15 +88,28 @@ def analyze_weather_trend(weather_data, temp_symbol, city_name=None):
|
|||||||
|
|
||||||
# === DEB 融合渲染 ===
|
# === DEB 融合渲染 ===
|
||||||
if city_name and current_forecasts:
|
if city_name and current_forecasts:
|
||||||
blended_high, weight_info = calculate_dynamic_weights(city_name, current_forecasts)
|
blended_high, weight_info = calculate_dynamic_weights(
|
||||||
|
city_name, current_forecasts
|
||||||
|
)
|
||||||
if blended_high is not None:
|
if blended_high is not None:
|
||||||
insights.insert(0, f"🧬 <b>DEB 融合预测</b>:<b>{blended_high}{temp_symbol}</b> ({weight_info})")
|
insights.insert(
|
||||||
ai_features.append(f"🧬 DEB系统已通过历史偏差矫正算出期待点是: {blended_high}{temp_symbol}。")
|
0,
|
||||||
|
f"🧬 <b>DEB 融合预测</b>:<b>{blended_high}{temp_symbol}</b> ({weight_info})",
|
||||||
|
)
|
||||||
|
ai_features.append(
|
||||||
|
f"🧬 DEB系统已通过历史偏差矫正算出期待点是: {blended_high}{temp_symbol}。"
|
||||||
|
)
|
||||||
|
|
||||||
# 顺便把今天的预测记录下来供之后回测用
|
# 顺便把今天的预测记录下来供之后回测用
|
||||||
try:
|
try:
|
||||||
update_daily_record(city_name, local_date_str, current_forecasts, max_so_far, deb_prediction=blended_high)
|
update_daily_record(
|
||||||
except:
|
city_name,
|
||||||
|
local_date_str,
|
||||||
|
current_forecasts,
|
||||||
|
max_so_far,
|
||||||
|
deb_prediction=blended_high,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# === METAR 趋势分析 (移到前部判断降温) ===
|
# === METAR 趋势分析 (移到前部判断降温) ===
|
||||||
@@ -103,16 +122,31 @@ def analyze_weather_trend(weather_data, temp_symbol, city_name=None):
|
|||||||
diff = latest_val - prev_val
|
diff = latest_val - prev_val
|
||||||
if len(temps_only) >= 3:
|
if len(temps_only) >= 3:
|
||||||
all_same = all(t == latest_val for t in temps_only[:3])
|
all_same = all(t == latest_val for t in temps_only[:3])
|
||||||
all_rising = all(temps_only[i] >= temps_only[i+1] for i in range(min(3, len(temps_only)) - 1))
|
all_rising = all(
|
||||||
all_falling = all(temps_only[i] <= temps_only[i+1] for i in range(min(3, len(temps_only)) - 1))
|
temps_only[i] >= temps_only[i + 1]
|
||||||
trend_display = " → ".join([f"{t}{temp_symbol}@{tm}" for tm, t in recent_temps[:3]])
|
for i in range(min(3, len(temps_only)) - 1)
|
||||||
if all_same: trend_desc = f"📉 温度已停滞({trend_display}),大概率到顶。"
|
)
|
||||||
elif all_rising and diff > 0: trend_desc = f"📈 仍在升温({trend_display})。"
|
all_falling = all(
|
||||||
elif all_falling and diff < 0: trend_desc = f"📉 已开始降温({trend_display})。"
|
temps_only[i] <= temps_only[i + 1]
|
||||||
else: trend_desc = f"📊 温度波动中({trend_display})。"
|
for i in range(min(3, len(temps_only)) - 1)
|
||||||
elif diff == 0: trend_desc = f"📉 温度持平(最近两条都是 {latest_val}{temp_symbol})。"
|
)
|
||||||
elif diff > 0: trend_desc = f"📈 仍在升温({prev_val} → {latest_val}{temp_symbol})。"
|
trend_display = " → ".join(
|
||||||
else: trend_desc = f"📉 已开始降温({prev_val} → {latest_val}{temp_symbol})。"
|
[f"{t}{temp_symbol}@{tm}" for tm, t in recent_temps[:3]]
|
||||||
|
)
|
||||||
|
if all_same:
|
||||||
|
trend_desc = f"📉 温度已停滞({trend_display}),大概率到顶。"
|
||||||
|
elif all_rising and diff > 0:
|
||||||
|
trend_desc = f"📈 仍在升温({trend_display})。"
|
||||||
|
elif all_falling and diff < 0:
|
||||||
|
trend_desc = f"📉 已开始降温({trend_display})。"
|
||||||
|
else:
|
||||||
|
trend_desc = f"📊 温度波动中({trend_display})。"
|
||||||
|
elif diff == 0:
|
||||||
|
trend_desc = f"📉 温度持平(最近两条都是 {latest_val}{temp_symbol})。"
|
||||||
|
elif diff > 0:
|
||||||
|
trend_desc = f"📈 仍在升温({prev_val} → {latest_val}{temp_symbol})。"
|
||||||
|
else:
|
||||||
|
trend_desc = f"📉 已开始降温({prev_val} → {latest_val}{temp_symbol})。"
|
||||||
|
|
||||||
is_cooling = "降温" in trend_desc
|
is_cooling = "降温" in trend_desc
|
||||||
|
|
||||||
@@ -140,11 +174,14 @@ def analyze_weather_trend(weather_data, temp_symbol, city_name=None):
|
|||||||
ens_median = _sf(ensemble.get("median"))
|
ens_median = _sf(ensemble.get("median"))
|
||||||
if ens_p10 is not None and ens_p90 is not None and ens_median is not None:
|
if ens_p10 is not None and ens_p90 is not None and ens_median is not None:
|
||||||
msg1 = f"📊 <b>集合预报</b>:中位数 {ens_median}{temp_symbol},90% 区间 [{ens_p10}{temp_symbol} - {ens_p90}{temp_symbol}]。"
|
msg1 = f"📊 <b>集合预报</b>:中位数 {ens_median}{temp_symbol},90% 区间 [{ens_p10}{temp_symbol} - {ens_p90}{temp_symbol}]。"
|
||||||
if not is_cooling: insights.append(msg1)
|
if not is_cooling:
|
||||||
|
insights.append(msg1)
|
||||||
ai_features.append(msg1)
|
ai_features.append(msg1)
|
||||||
|
|
||||||
if om_today is not None:
|
if om_today is not None:
|
||||||
if om_today > ens_p90 and (max_so_far is None or max_so_far < om_today - 0.5):
|
if om_today > ens_p90 and (
|
||||||
|
max_so_far is None or max_so_far < om_today - 0.5
|
||||||
|
):
|
||||||
msg2 = f"⚡ 预报偏高:确定性预报 {om_today}{temp_symbol} 超集合90%上限,更可能接近 {ens_median}{temp_symbol}。"
|
msg2 = f"⚡ 预报偏高:确定性预报 {om_today}{temp_symbol} 超集合90%上限,更可能接近 {ens_median}{temp_symbol}。"
|
||||||
ai_features.append(msg2)
|
ai_features.append(msg2)
|
||||||
elif om_today < ens_p10 and (max_so_far is None or max_so_far < ens_median):
|
elif om_today < ens_p10 and (max_so_far is None or max_so_far < ens_median):
|
||||||
@@ -153,14 +190,17 @@ def analyze_weather_trend(weather_data, temp_symbol, city_name=None):
|
|||||||
|
|
||||||
# === 数学概率计算(基于集合预报正态分布拟合)===
|
# === 数学概率计算(基于集合预报正态分布拟合)===
|
||||||
import math as _math
|
import math as _math
|
||||||
|
|
||||||
# 用 P10/P90 反推标准差: P10 = median - 1.28*sigma, P90 = median + 1.28*sigma
|
# 用 P10/P90 反推标准差: P10 = median - 1.28*sigma, P90 = median + 1.28*sigma
|
||||||
sigma = (ens_p90 - ens_p10) / 2.56
|
sigma = (ens_p90 - ens_p10) / 2.56
|
||||||
if sigma < 0.1: sigma = 0.1 # 防止除以零
|
if sigma < 0.1:
|
||||||
|
sigma = 0.1 # 防止除以零
|
||||||
|
|
||||||
# 用 DEB 历史 MAE 作为 σ 的下限
|
# 用 DEB 历史 MAE 作为 σ 的下限
|
||||||
# 如果模型过去的平均误差远大于集合预报的 σ,说明集合低估了真实不确定性
|
# 如果模型过去的平均误差远大于集合预报的 σ,说明集合低估了真实不确定性
|
||||||
if city_name:
|
if city_name:
|
||||||
from src.analysis.deb_algorithm import get_deb_accuracy
|
from src.analysis.deb_algorithm import get_deb_accuracy
|
||||||
|
|
||||||
acc = get_deb_accuracy(city_name)
|
acc = get_deb_accuracy(city_name)
|
||||||
if acc:
|
if acc:
|
||||||
_, hist_mae, _, _ = acc
|
_, hist_mae, _, _ = acc
|
||||||
@@ -183,7 +223,8 @@ def analyze_weather_trend(weather_data, temp_symbol, city_name=None):
|
|||||||
wspd_new = _sf(newest.get("wspd")) or 0
|
wspd_new = _sf(newest.get("wspd")) or 0
|
||||||
if wdir_old is not None and wdir_new is not None:
|
if wdir_old is not None and wdir_new is not None:
|
||||||
angle_diff = abs(wdir_new - wdir_old)
|
angle_diff = abs(wdir_new - wdir_old)
|
||||||
if angle_diff > 180: angle_diff = 360 - angle_diff
|
if angle_diff > 180:
|
||||||
|
angle_diff = 360 - angle_diff
|
||||||
wind_weight = min(wspd_new / 15.0, 1.0)
|
wind_weight = min(wspd_new / 15.0, 1.0)
|
||||||
wind_shock = min(angle_diff / 90.0, 1.0) * wind_weight * 0.4
|
wind_shock = min(angle_diff / 90.0, 1.0) * wind_weight * 0.4
|
||||||
shock_score += wind_shock
|
shock_score += wind_shock
|
||||||
@@ -207,7 +248,7 @@ def analyze_weather_trend(weather_data, temp_symbol, city_name=None):
|
|||||||
|
|
||||||
# 应用 shock_score: 放宽 σ
|
# 应用 shock_score: 放宽 σ
|
||||||
if shock_score > 0.05:
|
if shock_score > 0.05:
|
||||||
sigma *= (1 + 0.5 * shock_score)
|
sigma *= 1 + 0.5 * shock_score
|
||||||
|
|
||||||
# 时间修正:根据当前时间距峰值的位置调整 σ
|
# 时间修正:根据当前时间距峰值的位置调整 σ
|
||||||
# 峰值前:σ 不变(不确定性最大)
|
# 峰值前:σ 不变(不确定性最大)
|
||||||
@@ -231,7 +272,11 @@ def analyze_weather_trend(weather_data, temp_symbol, city_name=None):
|
|||||||
|
|
||||||
if ens_p10 is not None and ens_p90 is not None and not is_dead_market:
|
if ens_p10 is not None and ens_p90 is not None and not is_dead_market:
|
||||||
# (现有概率计算逻辑保留,但增加 is_dead_market 排除)
|
# (现有概率计算逻辑保留,但增加 is_dead_market 排除)
|
||||||
mu = forecast_median * 0.7 + ens_median * 0.3 if forecast_median is not None else ens_median
|
mu = (
|
||||||
|
forecast_median * 0.7 + ens_median * 0.3
|
||||||
|
if forecast_median is not None
|
||||||
|
else ens_median
|
||||||
|
)
|
||||||
if max_so_far is not None and max_so_far > mu:
|
if max_so_far is not None and max_so_far > mu:
|
||||||
mu = max_so_far + (0.3 if not is_cooling else 0.0)
|
mu = max_so_far + (0.3 if not is_cooling else 0.0)
|
||||||
|
|
||||||
@@ -241,15 +286,20 @@ def analyze_weather_trend(weather_data, temp_symbol, city_name=None):
|
|||||||
min_possible_wu = round(max_so_far) if max_so_far is not None else -999
|
min_possible_wu = round(max_so_far) if max_so_far is not None else -999
|
||||||
probs = {}
|
probs = {}
|
||||||
for n in range(round(mu) - 2, round(mu) + 3):
|
for n in range(round(mu) - 2, round(mu) + 3):
|
||||||
if n < min_possible_wu: continue
|
if n < min_possible_wu:
|
||||||
|
continue
|
||||||
p = _norm_cdf(n + 0.5, mu, sigma) - _norm_cdf(n - 0.5, mu, sigma)
|
p = _norm_cdf(n + 0.5, mu, sigma) - _norm_cdf(n - 0.5, mu, sigma)
|
||||||
if p > 0.01: probs[n] = p
|
if p > 0.01:
|
||||||
|
probs[n] = p
|
||||||
|
|
||||||
total_p = sum(probs.values())
|
total_p = sum(probs.values())
|
||||||
if total_p > 0:
|
if total_p > 0:
|
||||||
probs = {k: v / total_p for k, v in probs.items()}
|
probs = {k: v / total_p for k, v in probs.items()}
|
||||||
sorted_probs = sorted(probs.items(), key=lambda x: x[1], reverse=True)
|
sorted_probs = sorted(probs.items(), key=lambda x: x[1], reverse=True)
|
||||||
prob_parts = [f"{int(t)}{temp_symbol} [{t-0.5}~{t+0.5}) {p*100:.0f}%" for t, p in sorted_probs[:4]]
|
prob_parts = [
|
||||||
|
f"{int(t)}{temp_symbol} [{t - 0.5}~{t + 0.5}) {p * 100:.0f}%"
|
||||||
|
for t, p in sorted_probs[:4]
|
||||||
|
]
|
||||||
if prob_parts:
|
if prob_parts:
|
||||||
prob_str = " | ".join(prob_parts)
|
prob_str = " | ".join(prob_parts)
|
||||||
insights.append(f"🎲 <b>结算概率</b> (μ={mu:.1f}):{prob_str}")
|
insights.append(f"🎲 <b>结算概率</b> (μ={mu:.1f}):{prob_str}")
|
||||||
@@ -258,7 +308,7 @@ def analyze_weather_trend(weather_data, temp_symbol, city_name=None):
|
|||||||
settled_wu = round(max_so_far) if max_so_far is not None else "N/A"
|
settled_wu = round(max_so_far) if max_so_far is not None else "N/A"
|
||||||
dead_msg = f"🎲 <b>结算预测</b>:已锁定 {settled_wu}{temp_symbol} (死盘确认)"
|
dead_msg = f"🎲 <b>结算预测</b>:已锁定 {settled_wu}{temp_symbol} (死盘确认)"
|
||||||
insights.append(dead_msg)
|
insights.append(dead_msg)
|
||||||
ai_features.append(f"🎲 状态: 确认死盘,结算已无悬念。")
|
ai_features.append("🎲 状态: 确认死盘,结算已无悬念。")
|
||||||
|
|
||||||
# === 实测已超预报 & 趋势输出 ===
|
# === 实测已超预报 & 趋势输出 ===
|
||||||
if max_so_far is not None and forecast_high is not None:
|
if max_so_far is not None and forecast_high is not None:
|
||||||
@@ -266,8 +316,11 @@ def analyze_weather_trend(weather_data, temp_symbol, city_name=None):
|
|||||||
exceed_by = max_so_far - forecast_high
|
exceed_by = max_so_far - forecast_high
|
||||||
bt_msg = f"🚨 <b>实测已超预报</b>:{max_so_far}{temp_symbol} 超过上限 {forecast_high}{temp_symbol}(+{exceed_by:.1f}°)。"
|
bt_msg = f"🚨 <b>实测已超预报</b>:{max_so_far}{temp_symbol} 超过上限 {forecast_high}{temp_symbol}(+{exceed_by:.1f}°)。"
|
||||||
insights.append(bt_msg)
|
insights.append(bt_msg)
|
||||||
ai_features.append(f"🚨 异常: 实测已冲破所有预报上限 ({max_so_far}{temp_symbol} vs {forecast_high}{temp_symbol})。")
|
ai_features.append(
|
||||||
if trend_desc: ai_features.append(trend_desc)
|
f"🚨 异常: 实测已冲破所有预报上限 ({max_so_far}{temp_symbol} vs {forecast_high}{temp_symbol})。"
|
||||||
|
)
|
||||||
|
if trend_desc:
|
||||||
|
ai_features.append(trend_desc)
|
||||||
else:
|
else:
|
||||||
if trend_desc:
|
if trend_desc:
|
||||||
ai_features.append(trend_desc)
|
ai_features.append(trend_desc)
|
||||||
@@ -289,21 +342,39 @@ def analyze_weather_trend(weather_data, temp_symbol, city_name=None):
|
|||||||
|
|
||||||
# === 峰值时刻 AI 提示 ===
|
# === 峰值时刻 AI 提示 ===
|
||||||
if peak_hours:
|
if peak_hours:
|
||||||
window = f"{peak_hours[0]} - {peak_hours[-1]}" if len(peak_hours) > 1 else peak_hours[0]
|
window = (
|
||||||
|
f"{peak_hours[0]} - {peak_hours[-1]}"
|
||||||
|
if len(peak_hours) > 1
|
||||||
|
else peak_hours[0]
|
||||||
|
)
|
||||||
|
|
||||||
if local_hour <= last_peak_h:
|
if local_hour <= last_peak_h:
|
||||||
if last_peak_h < 6:
|
if last_peak_h < 6:
|
||||||
ai_features.append(f"⚠️ <b>提示</b>:预测最热在凌晨,后续气温可能一路走低。")
|
ai_features.append(
|
||||||
elif local_hour < first_peak_h and (max_so_far is None or max_so_far < forecast_high):
|
"⚠️ <b>提示</b>:预测最热在凌晨,后续气温可能一路走低。"
|
||||||
|
)
|
||||||
|
elif local_hour < first_peak_h and (
|
||||||
|
max_so_far is None or max_so_far < forecast_high
|
||||||
|
):
|
||||||
target_temp = om_today if om_today is not None else forecast_high
|
target_temp = om_today if om_today is not None else forecast_high
|
||||||
ai_features.append(f"🎯 <b>关注重点</b>:看看那个时段能否涨到 {target_temp}{temp_symbol}。")
|
ai_features.append(
|
||||||
|
f"🎯 <b>关注重点</b>:看看那个时段能否涨到 {target_temp}{temp_symbol}。"
|
||||||
|
)
|
||||||
|
|
||||||
# 写给AI(使用精确到分钟的时间)
|
# 写给AI(使用精确到分钟的时间)
|
||||||
remain_hrs = first_peak_h - local_hour_frac
|
remain_hrs = first_peak_h - local_hour_frac
|
||||||
if local_hour_frac > last_peak_h: ai_features.append(f"⏱️ 状态: 预报峰值时段已过 ({window})。")
|
if local_hour_frac > last_peak_h:
|
||||||
elif first_peak_h <= local_hour_frac <= last_peak_h: ai_features.append(f"⏱️ 状态: 正处于预报最热窗口 ({window})内。")
|
ai_features.append(f"⏱️ 状态: 预报峰值时段已过 ({window})。")
|
||||||
elif remain_hrs < 1: ai_features.append(f"⏱️ 状态: 距最热时段仅剩约 {int(remain_hrs * 60)} 分钟 ({window})。")
|
elif first_peak_h <= local_hour_frac <= last_peak_h:
|
||||||
else: ai_features.append(f"⏱️ 状态: 距最热时段还有约 {remain_hrs:.1f}h ({window})。")
|
ai_features.append(f"⏱️ 状态: 正处于预报最热窗口 ({window})内。")
|
||||||
|
elif remain_hrs < 1:
|
||||||
|
ai_features.append(
|
||||||
|
f"⏱️ 状态: 距最热时段仅剩约 {int(remain_hrs * 60)} 分钟 ({window})。"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
ai_features.append(
|
||||||
|
f"⏱️ 状态: 距最热时段还有约 {remain_hrs:.1f}h ({window})。"
|
||||||
|
)
|
||||||
|
|
||||||
# === 其他 AI 专供的事实特征 ===
|
# === 其他 AI 专供的事实特征 ===
|
||||||
# 明确告知 AI 当前实测温度和今日最高温,避免 AI 从趋势数据中误读
|
# 明确告知 AI 当前实测温度和今日最高温,避免 AI 从趋势数据中误读
|
||||||
@@ -311,10 +382,13 @@ def analyze_weather_trend(weather_data, temp_symbol, city_name=None):
|
|||||||
if current_temp is not None:
|
if current_temp is not None:
|
||||||
ai_features.append(f"🌡️ 当前实测温度: {current_temp}{temp_symbol}。")
|
ai_features.append(f"🌡️ 当前实测温度: {current_temp}{temp_symbol}。")
|
||||||
if max_so_far is not None:
|
if max_so_far is not None:
|
||||||
ai_features.append(f"🏔️ 今日实测最高温: {max_so_far}{temp_symbol} (WU结算={round(max_so_far)}{temp_symbol})。")
|
ai_features.append(
|
||||||
|
f"🏔️ 今日实测最高温: {max_so_far}{temp_symbol} (WU结算={round(max_so_far)}{temp_symbol})。"
|
||||||
|
)
|
||||||
|
|
||||||
# 传递城市的 METAR 取整特性给 AI
|
# 传递城市的 METAR 取整特性给 AI
|
||||||
from src.data_collection.city_risk_profiles import get_city_risk_profile
|
from src.data_collection.city_risk_profiles import get_city_risk_profile
|
||||||
|
|
||||||
if city_name:
|
if city_name:
|
||||||
_profile = get_city_risk_profile(city_name)
|
_profile = get_city_risk_profile(city_name)
|
||||||
if _profile and _profile.get("metar_rounding"):
|
if _profile and _profile.get("metar_rounding"):
|
||||||
@@ -323,16 +397,20 @@ def analyze_weather_trend(weather_data, temp_symbol, city_name=None):
|
|||||||
wind_dir = metar.get("current", {}).get("wind_dir", "未知")
|
wind_dir = metar.get("current", {}).get("wind_dir", "未知")
|
||||||
ai_features.append(f"🌬️ 当下风况: 约 {wind_speed}kt (方向 {wind_dir}°)。")
|
ai_features.append(f"🌬️ 当下风况: 约 {wind_speed}kt (方向 {wind_dir}°)。")
|
||||||
humidity = metar.get("current", {}).get("humidity")
|
humidity = metar.get("current", {}).get("humidity")
|
||||||
if humidity and humidity > 80: ai_features.append(f"💦 湿度极高 ({humidity}%)。")
|
if humidity and humidity > 80:
|
||||||
|
ai_features.append(f"💦 湿度极高 ({humidity}%)。")
|
||||||
|
|
||||||
clouds = metar.get("current", {}).get("clouds", [])
|
clouds = metar.get("current", {}).get("clouds", [])
|
||||||
if clouds:
|
if clouds:
|
||||||
cover = clouds[-1].get("cover", "")
|
cover = clouds[-1].get("cover", "")
|
||||||
c_desc = {"OVC": "全阴", "BKN": "多云", "SCT": "散云", "FEW": "少云"}.get(cover, cover)
|
c_desc = {"OVC": "全阴", "BKN": "多云", "SCT": "散云", "FEW": "少云"}.get(
|
||||||
|
cover, cover
|
||||||
|
)
|
||||||
ai_features.append(f"☁️ 天空状况: {c_desc}。")
|
ai_features.append(f"☁️ 天空状况: {c_desc}。")
|
||||||
|
|
||||||
wx_desc = metar.get("current", {}).get("wx_desc")
|
wx_desc = metar.get("current", {}).get("wx_desc")
|
||||||
if wx_desc: ai_features.append(f"🌧️ 天气现象: {wx_desc}。")
|
if wx_desc:
|
||||||
|
ai_features.append(f"🌧️ 天气现象: {wx_desc}。")
|
||||||
|
|
||||||
max_temp_time_str = metar.get("current", {}).get("max_temp_time", "")
|
max_temp_time_str = metar.get("current", {}).get("max_temp_time", "")
|
||||||
if max_so_far is not None and max_temp_time_str:
|
if max_so_far is not None and max_temp_time_str:
|
||||||
@@ -341,16 +419,23 @@ def analyze_weather_trend(weather_data, temp_symbol, city_name=None):
|
|||||||
max_temp_rad = 0.0
|
max_temp_rad = 0.0
|
||||||
hourly_rad = hourly.get("shortwave_radiation", [])
|
hourly_rad = hourly.get("shortwave_radiation", [])
|
||||||
for t_str, rad in zip(times, hourly_rad):
|
for t_str, rad in zip(times, hourly_rad):
|
||||||
if t_str.startswith(local_date_str) and int(t_str.split("T")[1][:2]) == max_h:
|
if (
|
||||||
|
t_str.startswith(local_date_str)
|
||||||
|
and int(t_str.split("T")[1][:2]) == max_h
|
||||||
|
):
|
||||||
max_temp_rad = rad if rad is not None else 0.0
|
max_temp_rad = rad if rad is not None else 0.0
|
||||||
break
|
break
|
||||||
if max_temp_rad < 50:
|
if max_temp_rad < 50:
|
||||||
ai_features.append(f"🌙 动力事实: 最高温出现在低辐射时段 ({max_temp_time_str}, 辐射{max_temp_rad:.0f}W/m²)。")
|
ai_features.append(
|
||||||
except: pass
|
f"🌙 动力事实: 最高温出现在低辐射时段 ({max_temp_time_str}, 辐射{max_temp_rad:.0f}W/m²)。"
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
display_str = "\n".join(insights) if insights else ""
|
display_str = "\n".join(insights) if insights else ""
|
||||||
return display_str, "\n".join(ai_features)
|
return display_str, "\n".join(ai_features)
|
||||||
|
|
||||||
|
|
||||||
def start_bot():
|
def start_bot():
|
||||||
config = load_config()
|
config = load_config()
|
||||||
token = os.getenv("TELEGRAM_BOT_TOKEN")
|
token = os.getenv("TELEGRAM_BOT_TOKEN")
|
||||||
@@ -387,34 +472,47 @@ def start_bot():
|
|||||||
try:
|
try:
|
||||||
parts = message.text.split(maxsplit=1)
|
parts = message.text.split(maxsplit=1)
|
||||||
if len(parts) < 2:
|
if len(parts) < 2:
|
||||||
bot.reply_to(message, "❓ 用法: <code>/deb ankara</code>", parse_mode="HTML")
|
bot.reply_to(
|
||||||
|
message, "❓ 用法: <code>/deb ankara</code>", parse_mode="HTML"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
city_input = parts[1].strip().lower()
|
city_input = parts[1].strip().lower()
|
||||||
# 复用城市名映射
|
# 复用城市名映射
|
||||||
city_aliases = {
|
city_aliases = {
|
||||||
"ank": "ankara", "lon": "london", "par": "paris",
|
"ank": "ankara",
|
||||||
"nyc": "new york", "chi": "chicago", "dal": "dallas",
|
"lon": "london",
|
||||||
"mia": "miami", "atl": "atlanta", "sea": "seattle",
|
"par": "paris",
|
||||||
"tor": "toronto", "sel": "seoul", "ba": "buenos aires",
|
"nyc": "new york",
|
||||||
|
"chi": "chicago",
|
||||||
|
"dal": "dallas",
|
||||||
|
"mia": "miami",
|
||||||
|
"atl": "atlanta",
|
||||||
|
"sea": "seattle",
|
||||||
|
"tor": "toronto",
|
||||||
|
"sel": "seoul",
|
||||||
|
"ba": "buenos aires",
|
||||||
"wel": "wellington",
|
"wel": "wellington",
|
||||||
}
|
}
|
||||||
city_name = city_aliases.get(city_input, city_input)
|
city_name = city_aliases.get(city_input, city_input)
|
||||||
|
|
||||||
from src.analysis.deb_algorithm import get_deb_accuracy, load_history
|
from src.analysis.deb_algorithm import load_history
|
||||||
import os as _os
|
import os as _os
|
||||||
|
|
||||||
# 获取详细历史数据
|
# 获取详细历史数据
|
||||||
project_root = _os.path.dirname(_os.path.abspath(__file__))
|
project_root = _os.path.dirname(_os.path.abspath(__file__))
|
||||||
history_file = _os.path.join(project_root, 'data', 'daily_records.json')
|
history_file = _os.path.join(project_root, "data", "daily_records.json")
|
||||||
data = load_history(history_file)
|
data = load_history(history_file)
|
||||||
|
|
||||||
if city_name not in data or not data[city_name]:
|
if city_name not in data or not data[city_name]:
|
||||||
bot.reply_to(message, f"❌ 暂无 {city_name} 的历史数据", parse_mode="HTML")
|
bot.reply_to(
|
||||||
|
message, f"❌ 暂无 {city_name} 的历史数据", parse_mode="HTML"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
city_data = data[city_name]
|
city_data = data[city_name]
|
||||||
from datetime import datetime as _dt
|
from datetime import datetime as _dt
|
||||||
|
|
||||||
today_str = _dt.now().strftime("%Y-%m-%d")
|
today_str = _dt.now().strftime("%Y-%m-%d")
|
||||||
|
|
||||||
lines = [f"📊 <b>DEB 准确率报告 - {city_name.title()}</b>\n"]
|
lines = [f"📊 <b>DEB 准确率报告 - {city_name.title()}</b>\n"]
|
||||||
@@ -429,20 +527,24 @@ def start_bot():
|
|||||||
|
|
||||||
for date_str in sorted(city_data.keys()):
|
for date_str in sorted(city_data.keys()):
|
||||||
record = city_data[date_str]
|
record = city_data[date_str]
|
||||||
actual = record.get('actual_high')
|
actual = record.get("actual_high")
|
||||||
deb_pred = record.get('deb_prediction')
|
deb_pred = record.get("deb_prediction")
|
||||||
forecasts = record.get('forecasts', {})
|
forecasts = record.get("forecasts", {})
|
||||||
|
|
||||||
if actual is None:
|
if actual is None:
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
actual = float(actual)
|
actual = float(actual)
|
||||||
if deb_pred is not None: deb_pred = float(deb_pred)
|
if deb_pred is not None:
|
||||||
except: continue
|
deb_pred = float(deb_pred)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
# 如果没有存 DEB 预测值,用当天各模型平均值回算
|
# 如果没有存 DEB 预测值,用当天各模型平均值回算
|
||||||
if deb_pred is None and forecasts:
|
if deb_pred is None and forecasts:
|
||||||
valid_preds = [float(v) for v in forecasts.values() if v is not None]
|
valid_preds = [
|
||||||
|
float(v) for v in forecasts.values() if v is not None
|
||||||
|
]
|
||||||
if valid_preds:
|
if valid_preds:
|
||||||
deb_pred = round(sum(valid_preds) / len(valid_preds), 1)
|
deb_pred = round(sum(valid_preds) / len(valid_preds), 1)
|
||||||
|
|
||||||
@@ -453,18 +555,25 @@ def start_bot():
|
|||||||
total_days += 1
|
total_days += 1
|
||||||
deb_wu = round(deb_pred)
|
deb_wu = round(deb_pred)
|
||||||
hit = deb_wu == actual_wu
|
hit = deb_wu == actual_wu
|
||||||
if hit: hits += 1
|
if hit:
|
||||||
|
hits += 1
|
||||||
err = deb_pred - actual
|
err = deb_pred - actual
|
||||||
deb_errors.append(abs(err))
|
deb_errors.append(abs(err))
|
||||||
signed_errors.append(err)
|
signed_errors.append(err)
|
||||||
icon = "✅" if hit else "❌"
|
icon = "✅" if hit else "❌"
|
||||||
retro = "≈" if 'deb_prediction' not in record else ""
|
retro = "≈" if "deb_prediction" not in record else ""
|
||||||
# 错误类型标签
|
# 错误类型标签
|
||||||
if not hit:
|
if not hit:
|
||||||
err_label = f" 低估{abs(err):.1f}°" if err < 0 else f" 高估{abs(err):.1f}°"
|
err_label = (
|
||||||
|
f" 低估{abs(err):.1f}°"
|
||||||
|
if err < 0
|
||||||
|
else f" 高估{abs(err):.1f}°"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
err_label = f" 偏差{abs(err):.1f}°"
|
err_label = f" 偏差{abs(err):.1f}°"
|
||||||
lines.append(f" {date_str}: DEB {retro}{deb_pred}→<b>{deb_wu}</b> vs 实测 {actual}→<b>{actual_wu}</b> {icon}{err_label}")
|
lines.append(
|
||||||
|
f" {date_str}: DEB {retro}{deb_pred}→<b>{deb_wu}</b> vs 实测 {actual}→<b>{actual_wu}</b> {icon}{err_label}"
|
||||||
|
)
|
||||||
elif date_str == today_str:
|
elif date_str == today_str:
|
||||||
lines.append(f" {date_str}: 📍 今天进行中 (实测暂 {actual})")
|
lines.append(f" {date_str}: 📍 今天进行中 (实测暂 {actual})")
|
||||||
|
|
||||||
@@ -480,12 +589,16 @@ def start_bot():
|
|||||||
if total_days > 0:
|
if total_days > 0:
|
||||||
hit_rate = hits / total_days * 100
|
hit_rate = hits / total_days * 100
|
||||||
deb_mae = sum(deb_errors) / len(deb_errors)
|
deb_mae = sum(deb_errors) / len(deb_errors)
|
||||||
lines.append(f"\n🎯 <b>DEB 总战绩</b>:WU命中 {hits}/{total_days} (<b>{hit_rate:.0f}%</b>) | MAE: {deb_mae:.1f}°")
|
lines.append(
|
||||||
|
f"\n🎯 <b>DEB 总战绩</b>:WU命中 {hits}/{total_days} (<b>{hit_rate:.0f}%</b>) | MAE: {deb_mae:.1f}°"
|
||||||
|
)
|
||||||
|
|
||||||
# 和各模型 MAE 对比
|
# 和各模型 MAE 对比
|
||||||
if model_errors:
|
if model_errors:
|
||||||
lines.append(f"\n📈 <b>模型 MAE 对比</b>:")
|
lines.append("\n📈 <b>模型 MAE 对比</b>:")
|
||||||
model_maes = {m: sum(e)/len(e) for m, e in model_errors.items() if e}
|
model_maes = {
|
||||||
|
m: sum(e) / len(e) for m, e in model_errors.items() if e
|
||||||
|
}
|
||||||
sorted_models = sorted(model_maes.items(), key=lambda x: x[1])
|
sorted_models = sorted(model_maes.items(), key=lambda x: x[1])
|
||||||
for m, mae in sorted_models:
|
for m, mae in sorted_models:
|
||||||
tag = " ⭐" if mae <= deb_mae else ""
|
tag = " ⭐" if mae <= deb_mae else ""
|
||||||
@@ -497,28 +610,36 @@ def start_bot():
|
|||||||
underest = sum(1 for e in signed_errors if e < -0.3)
|
underest = sum(1 for e in signed_errors if e < -0.3)
|
||||||
overest = sum(1 for e in signed_errors if e > 0.3)
|
overest = sum(1 for e in signed_errors if e > 0.3)
|
||||||
|
|
||||||
lines.append(f"\n🔍 <b>偏差分析</b>:")
|
lines.append("\n🔍 <b>偏差分析</b>:")
|
||||||
if abs(mean_bias) > 0.3:
|
if abs(mean_bias) > 0.3:
|
||||||
bias_dir = "低估" if mean_bias < 0 else "高估"
|
bias_dir = "低估" if mean_bias < 0 else "高估"
|
||||||
lines.append(f" ⚠️ 系统性{bias_dir}:平均偏差 {mean_bias:+.1f}°")
|
lines.append(f" ⚠️ 系统性{bias_dir}:平均偏差 {mean_bias:+.1f}°")
|
||||||
else:
|
else:
|
||||||
lines.append(f" ✅ 无明显系统偏差(平均 {mean_bias:+.1f}°)")
|
lines.append(f" ✅ 无明显系统偏差(平均 {mean_bias:+.1f}°)")
|
||||||
lines.append(f" 低估 {underest} 次 | 高估 {overest} 次 | 准确 {total_days - underest - overest} 次")
|
lines.append(
|
||||||
|
f" 低估 {underest} 次 | 高估 {overest} 次 | 准确 {total_days - underest - overest} 次"
|
||||||
|
)
|
||||||
|
|
||||||
# 可操作建议
|
# 可操作建议
|
||||||
lines.append(f"\n💡 <b>建议</b>:")
|
lines.append("\n💡 <b>建议</b>:")
|
||||||
if underest > overest and abs(mean_bias) > 0.5:
|
if underest > overest and abs(mean_bias) > 0.5:
|
||||||
lines.append(f" 该城市模型集体低估趋势明显({mean_bias:+.1f}°),实际最高温可能比 DEB 融合值高 {abs(mean_bias):.0f}-{abs(mean_bias)+0.5:.0f}°。交易时建议适当看高。")
|
lines.append(
|
||||||
|
f" 该城市模型集体低估趋势明显({mean_bias:+.1f}°),实际最高温可能比 DEB 融合值高 {abs(mean_bias):.0f}-{abs(mean_bias) + 0.5:.0f}°。交易时建议适当看高。"
|
||||||
|
)
|
||||||
elif overest > underest and abs(mean_bias) > 0.5:
|
elif overest > underest and abs(mean_bias) > 0.5:
|
||||||
lines.append(f" 该城市模型集体高估趋势明显({mean_bias:+.1f}°),实际最高温可能比 DEB 融合值低。交易时建议适当看低。")
|
lines.append(
|
||||||
|
f" 该城市模型集体高估趋势明显({mean_bias:+.1f}°),实际最高温可能比 DEB 融合值低。交易时建议适当看低。"
|
||||||
|
)
|
||||||
elif deb_mae > 1.5:
|
elif deb_mae > 1.5:
|
||||||
lines.append(f" 该城市预报波动大 (MAE {deb_mae:.1f}°),建议观望或轻仓。")
|
lines.append(
|
||||||
|
f" 该城市预报波动大 (MAE {deb_mae:.1f}°),建议观望或轻仓。"
|
||||||
|
)
|
||||||
elif hit_rate >= 60:
|
elif hit_rate >= 60:
|
||||||
lines.append(f" DEB 表现良好,可作为主要参考。")
|
lines.append(" DEB 表现良好,可作为主要参考。")
|
||||||
else:
|
else:
|
||||||
lines.append(f" 数据积累中,建议结合 AI 分析综合判断。")
|
lines.append(" 数据积累中,建议结合 AI 分析综合判断。")
|
||||||
|
|
||||||
lines.append(f"\n📝 MAE = 平均绝对误差,越小越准。⭐ = 优于 DEB 融合。")
|
lines.append("\n📝 MAE = 平均绝对误差,越小越准。⭐ = 优于 DEB 融合。")
|
||||||
else:
|
else:
|
||||||
lines.append("\n⏳ 尚无完整的 DEB 预测记录,明天起开始统计。")
|
lines.append("\n⏳ 尚无完整的 DEB 预测记录,明天起开始统计。")
|
||||||
|
|
||||||
@@ -544,20 +665,36 @@ def start_bot():
|
|||||||
# --- 核心标准名称映射表 ---
|
# --- 核心标准名称映射表 ---
|
||||||
# 这里的 Key 是缩写或别名,Value 是 Open-Meteo 识别的标准全称
|
# 这里的 Key 是缩写或别名,Value 是 Open-Meteo 识别的标准全称
|
||||||
STANDARD_MAPPING = {
|
STANDARD_MAPPING = {
|
||||||
"sel": "seoul", "seo": "seoul", "首尔": "seoul",
|
"sel": "seoul",
|
||||||
"lon": "london", "伦敦": "london",
|
"seo": "seoul",
|
||||||
"tor": "toronto", "多伦多": "toronto",
|
"首尔": "seoul",
|
||||||
"ank": "ankara", "安卡拉": "ankara",
|
"lon": "london",
|
||||||
"wel": "wellington", "惠灵顿": "wellington",
|
"伦敦": "london",
|
||||||
"ba": "buenos aires", "布宜诺斯艾利斯": "buenos aires",
|
"tor": "toronto",
|
||||||
"nyc": "new york", "ny": "new york", "纽约": "new york",
|
"多伦多": "toronto",
|
||||||
"chi": "chicago", "芝加哥": "chicago",
|
"ank": "ankara",
|
||||||
"sea": "seattle", "西雅图": "seattle",
|
"安卡拉": "ankara",
|
||||||
"mia": "miami", "迈阿密": "miami",
|
"wel": "wellington",
|
||||||
"atl": "atlanta", "亚特兰大": "atlanta",
|
"惠灵顿": "wellington",
|
||||||
"dal": "dallas", "达拉斯": "dallas",
|
"ba": "buenos aires",
|
||||||
"la": "los angeles", "洛杉矶": "los angeles",
|
"布宜诺斯艾利斯": "buenos aires",
|
||||||
"par": "paris", "巴黎": "paris",
|
"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",
|
||||||
}
|
}
|
||||||
|
|
||||||
# 支持的城市全名列表(用于模糊匹配)
|
# 支持的城市全名列表(用于模糊匹配)
|
||||||
@@ -596,23 +733,30 @@ def start_bot():
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
bot.send_message(message.chat.id, f"🔍 正在查询 {city_name.title()} 的天气数据...")
|
bot.send_message(
|
||||||
|
message.chat.id, f"🔍 正在查询 {city_name.title()} 的天气数据..."
|
||||||
|
)
|
||||||
|
|
||||||
coords = weather.get_coordinates(city_name)
|
coords = weather.get_coordinates(city_name)
|
||||||
if not coords:
|
if not coords:
|
||||||
bot.reply_to(message, f"❌ 未找到城市坐标: {city_name}")
|
bot.reply_to(message, f"❌ 未找到城市坐标: {city_name}")
|
||||||
return
|
return
|
||||||
|
|
||||||
weather_data = weather.fetch_all_sources(city_name, lat=coords["lat"], lon=coords["lon"])
|
weather_data = weather.fetch_all_sources(
|
||||||
|
city_name, lat=coords["lat"], lon=coords["lon"]
|
||||||
|
)
|
||||||
open_meteo = weather_data.get("open-meteo", {})
|
open_meteo = weather_data.get("open-meteo", {})
|
||||||
metar = weather_data.get("metar", {})
|
metar = weather_data.get("metar", {})
|
||||||
mgm = weather_data.get("mgm", {})
|
mgm = weather_data.get("mgm", {})
|
||||||
|
|
||||||
# 数值归一化
|
# 数值归一化
|
||||||
def _sf(v):
|
def _sf(v):
|
||||||
if v is None: return None
|
if v is None:
|
||||||
try: return float(v)
|
return None
|
||||||
except: return None
|
try:
|
||||||
|
return float(v)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
temp_unit = open_meteo.get("unit", "celsius")
|
temp_unit = open_meteo.get("unit", "celsius")
|
||||||
temp_symbol = "°F" if temp_unit == "fahrenheit" else "°C"
|
temp_symbol = "°F" if temp_unit == "fahrenheit" else "°C"
|
||||||
@@ -630,7 +774,9 @@ def start_bot():
|
|||||||
# --- 2. 紧凑 风险提示 ---
|
# --- 2. 紧凑 风险提示 ---
|
||||||
if risk_profile:
|
if risk_profile:
|
||||||
bias = risk_profile.get("bias", "±0.0")
|
bias = risk_profile.get("bias", "±0.0")
|
||||||
msg_lines.append(f"⚠️ {risk_profile.get('airport_name', '')}: {bias}{temp_symbol} | {risk_profile.get('warning', '')}")
|
msg_lines.append(
|
||||||
|
f"⚠️ {risk_profile.get('airport_name', '')}: {bias}{temp_symbol} | {risk_profile.get('warning', '')}"
|
||||||
|
)
|
||||||
|
|
||||||
# --- 3. 紧凑 预测区 ---
|
# --- 3. 紧凑 预测区 ---
|
||||||
daily = open_meteo.get("daily", {})
|
daily = open_meteo.get("daily", {})
|
||||||
@@ -648,13 +794,25 @@ def start_bot():
|
|||||||
|
|
||||||
if mb_high is not None:
|
if mb_high is not None:
|
||||||
sources.append("MB")
|
sources.append("MB")
|
||||||
comp_parts.append(f"MB: {mb_high:.1f}{temp_symbol}" if isinstance(mb_high, (int, float)) else f"MB: {mb_high}")
|
comp_parts.append(
|
||||||
|
f"MB: {mb_high:.1f}{temp_symbol}"
|
||||||
|
if isinstance(mb_high, (int, float))
|
||||||
|
else f"MB: {mb_high}"
|
||||||
|
)
|
||||||
if nws_high is not None:
|
if nws_high is not None:
|
||||||
sources.append("NWS")
|
sources.append("NWS")
|
||||||
comp_parts.append(f"NWS: {nws_high:.1f}{temp_symbol}" if isinstance(nws_high, (int, float)) else f"NWS: {nws_high}")
|
comp_parts.append(
|
||||||
|
f"NWS: {nws_high:.1f}{temp_symbol}"
|
||||||
|
if isinstance(nws_high, (int, float))
|
||||||
|
else f"NWS: {nws_high}"
|
||||||
|
)
|
||||||
if mgm_high is not None:
|
if mgm_high is not None:
|
||||||
sources.append("MGM")
|
sources.append("MGM")
|
||||||
comp_parts.append(f"MGM: {mgm_high:.1f}{temp_symbol}" if isinstance(mgm_high, (int, float)) else f"MGM: {mgm_high}")
|
comp_parts.append(
|
||||||
|
f"MGM: {mgm_high:.1f}{temp_symbol}"
|
||||||
|
if isinstance(mgm_high, (int, float))
|
||||||
|
else f"MGM: {mgm_high}"
|
||||||
|
)
|
||||||
|
|
||||||
# 检查是否有显著分歧 (超过 5°F 或 2.5°C)
|
# 检查是否有显著分歧 (超过 5°F 或 2.5°C)
|
||||||
divergence_warning = ""
|
divergence_warning = ""
|
||||||
@@ -662,13 +820,17 @@ def start_bot():
|
|||||||
diff = abs(mb_high - (_sf(max_temps[0]) or 0))
|
diff = abs(mb_high - (_sf(max_temps[0]) or 0))
|
||||||
threshold = 5.0 if temp_unit == "fahrenheit" else 2.5
|
threshold = 5.0 if temp_unit == "fahrenheit" else 2.5
|
||||||
if diff > threshold:
|
if diff > threshold:
|
||||||
divergence_warning = f" ⚠️ <b>模型显著分歧 ({diff:.1f}{temp_symbol})</b>"
|
divergence_warning = (
|
||||||
|
f" ⚠️ <b>模型显著分歧 ({diff:.1f}{temp_symbol})</b>"
|
||||||
|
)
|
||||||
|
|
||||||
comp_str = f" ({' | '.join(comp_parts)})" if comp_parts else ""
|
comp_str = f" ({' | '.join(comp_parts)})" if comp_parts else ""
|
||||||
sources_str = " | ".join(sources)
|
sources_str = " | ".join(sources)
|
||||||
|
|
||||||
msg_lines.append(f"\n📊 <b>预报 ({sources_str})</b>")
|
msg_lines.append(f"\n📊 <b>预报 ({sources_str})</b>")
|
||||||
msg_lines.append(f"👉 <b>今天: {today_t}{temp_symbol}{comp_str}</b>{divergence_warning}")
|
msg_lines.append(
|
||||||
|
f"👉 <b>今天: {today_t}{temp_symbol}{comp_str}</b>{divergence_warning}"
|
||||||
|
)
|
||||||
|
|
||||||
# 明后天
|
# 明后天
|
||||||
if len(dates) > 1:
|
if len(dates) > 1:
|
||||||
@@ -682,8 +844,16 @@ def start_bot():
|
|||||||
sunsets = daily.get("sunset", [])
|
sunsets = daily.get("sunset", [])
|
||||||
sunshine_durations = daily.get("sunshine_duration", [])
|
sunshine_durations = daily.get("sunshine_duration", [])
|
||||||
if sunrises and sunsets:
|
if sunrises and sunsets:
|
||||||
sunrise_t = sunrises[0].split("T")[1][:5] if "T" in str(sunrises[0]) else sunrises[0]
|
sunrise_t = (
|
||||||
sunset_t = sunsets[0].split("T")[1][:5] if "T" in str(sunsets[0]) else sunsets[0]
|
sunrises[0].split("T")[1][:5]
|
||||||
|
if "T" in str(sunrises[0])
|
||||||
|
else sunrises[0]
|
||||||
|
)
|
||||||
|
sunset_t = (
|
||||||
|
sunsets[0].split("T")[1][:5]
|
||||||
|
if "T" in str(sunsets[0])
|
||||||
|
else sunsets[0]
|
||||||
|
)
|
||||||
sun_line = f"🌅 日出 {sunrise_t} | 🌇 日落 {sunset_t}"
|
sun_line = f"🌅 日出 {sunrise_t} | 🌇 日落 {sunset_t}"
|
||||||
if sunshine_durations:
|
if sunshine_durations:
|
||||||
sunshine_hours = sunshine_durations[0] / 3600 # 秒 -> 小时
|
sunshine_hours = sunshine_durations[0] / 3600 # 秒 -> 小时
|
||||||
@@ -692,9 +862,17 @@ def start_bot():
|
|||||||
|
|
||||||
# --- 4. 核心 实测区 (合并 METAR 和 MGM) ---
|
# --- 4. 核心 实测区 (合并 METAR 和 MGM) ---
|
||||||
# 基础数据优先用 METAR
|
# 基础数据优先用 METAR
|
||||||
cur_temp = _sf(metar.get("current", {}).get("temp") if metar else mgm.get("current", {}).get("temp"))
|
cur_temp = _sf(
|
||||||
max_p = _sf(metar.get("current", {}).get("max_temp_so_far") if metar else None)
|
metar.get("current", {}).get("temp")
|
||||||
max_p_time = metar.get("current", {}).get("max_temp_time") if metar else None
|
if metar
|
||||||
|
else mgm.get("current", {}).get("temp")
|
||||||
|
)
|
||||||
|
max_p = _sf(
|
||||||
|
metar.get("current", {}).get("max_temp_so_far") if metar else None
|
||||||
|
)
|
||||||
|
max_p_time = (
|
||||||
|
metar.get("current", {}).get("max_temp_time") if metar else None
|
||||||
|
)
|
||||||
obs_t_str = "N/A"
|
obs_t_str = "N/A"
|
||||||
metar_age_min = None # METAR 数据年龄(分钟)
|
metar_age_min = None # METAR 数据年龄(分钟)
|
||||||
main_source = "METAR" if metar else "MGM"
|
main_source = "METAR" if metar else "MGM"
|
||||||
@@ -704,9 +882,12 @@ def start_bot():
|
|||||||
try:
|
try:
|
||||||
if "T" in obs_t:
|
if "T" in obs_t:
|
||||||
from datetime import datetime, timezone, timedelta
|
from datetime import datetime, timezone, timedelta
|
||||||
|
|
||||||
dt = datetime.fromisoformat(obs_t.replace("Z", "+00:00"))
|
dt = datetime.fromisoformat(obs_t.replace("Z", "+00:00"))
|
||||||
utc_offset = open_meteo.get("utc_offset", 0)
|
utc_offset = open_meteo.get("utc_offset", 0)
|
||||||
local_dt = dt.astimezone(timezone(timedelta(seconds=utc_offset)))
|
local_dt = dt.astimezone(
|
||||||
|
timezone(timedelta(seconds=utc_offset))
|
||||||
|
)
|
||||||
obs_t_str = local_dt.strftime("%H:%M")
|
obs_t_str = local_dt.strftime("%H:%M")
|
||||||
# 计算数据年龄
|
# 计算数据年龄
|
||||||
now_utc = datetime.now(timezone.utc)
|
now_utc = datetime.now(timezone.utc)
|
||||||
@@ -715,14 +896,17 @@ def start_bot():
|
|||||||
obs_t_str = obs_t.split(" ")[1][:5]
|
obs_t_str = obs_t.split(" ")[1][:5]
|
||||||
else:
|
else:
|
||||||
obs_t_str = obs_t
|
obs_t_str = obs_t
|
||||||
except:
|
except Exception:
|
||||||
obs_t_str = obs_t[:16]
|
obs_t_str = obs_t[:16]
|
||||||
elif mgm:
|
elif mgm:
|
||||||
m_time = mgm.get("current", {}).get("time", "")
|
m_time = mgm.get("current", {}).get("time", "")
|
||||||
if "T" in m_time:
|
if "T" in m_time:
|
||||||
from datetime import datetime, timezone, timedelta
|
from datetime import datetime, timezone, timedelta
|
||||||
|
|
||||||
dt = datetime.fromisoformat(m_time.replace("Z", "+00:00"))
|
dt = datetime.fromisoformat(m_time.replace("Z", "+00:00"))
|
||||||
m_time = dt.astimezone(timezone(timedelta(hours=3))).strftime("%H:%M")
|
m_time = dt.astimezone(timezone(timedelta(hours=3))).strftime(
|
||||||
|
"%H:%M"
|
||||||
|
)
|
||||||
elif " " in m_time:
|
elif " " in m_time:
|
||||||
m_time = m_time.split(" ")[1][:5]
|
m_time = m_time.split(" ")[1][:5]
|
||||||
obs_t_str = m_time
|
obs_t_str = m_time
|
||||||
@@ -738,6 +922,7 @@ def start_bot():
|
|||||||
max_str = ""
|
max_str = ""
|
||||||
if max_p is not None:
|
if max_p is not None:
|
||||||
import math
|
import math
|
||||||
|
|
||||||
settled_val = math.floor(max_p + 0.5)
|
settled_val = math.floor(max_p + 0.5)
|
||||||
max_str = f" (最高: {max_p}{temp_symbol}"
|
max_str = f" (最高: {max_p}{temp_symbol}"
|
||||||
if max_p_time:
|
if max_p_time:
|
||||||
@@ -754,7 +939,17 @@ def start_bot():
|
|||||||
if metar_wx:
|
if metar_wx:
|
||||||
wx_upper = metar_wx.upper().strip()
|
wx_upper = metar_wx.upper().strip()
|
||||||
wx_tokens = set(wx_upper.split())
|
wx_tokens = set(wx_upper.split())
|
||||||
rain_codes = {"RA", "DZ", "-RA", "+RA", "-DZ", "+DZ", "TSRA", "SHRA", "FZRA"}
|
rain_codes = {
|
||||||
|
"RA",
|
||||||
|
"DZ",
|
||||||
|
"-RA",
|
||||||
|
"+RA",
|
||||||
|
"-DZ",
|
||||||
|
"+DZ",
|
||||||
|
"TSRA",
|
||||||
|
"SHRA",
|
||||||
|
"FZRA",
|
||||||
|
}
|
||||||
snow_codes = {"SN", "GR", "GS", "-SN", "+SN", "BLSN"}
|
snow_codes = {"SN", "GR", "GS", "-SN", "+SN", "BLSN"}
|
||||||
fog_codes = {"FG", "BR", "HZ", "FZFG"}
|
fog_codes = {"FG", "BR", "HZ", "FZFG"}
|
||||||
ts_codes = {"TS", "TSRA"}
|
ts_codes = {"TS", "TSRA"}
|
||||||
@@ -763,7 +958,9 @@ def start_bot():
|
|||||||
elif {"+RA", "+SN"} & wx_tokens:
|
elif {"+RA", "+SN"} & wx_tokens:
|
||||||
wx_summary = "🌧️ 大雨" if "+RA" in wx_tokens else "❄️ 大雪"
|
wx_summary = "🌧️ 大雨" if "+RA" in wx_tokens else "❄️ 大雪"
|
||||||
elif rain_codes & wx_tokens:
|
elif rain_codes & wx_tokens:
|
||||||
wx_summary = "🌧️ 小雨" if {"-RA", "-DZ", "DZ"} & wx_tokens else "🌧️ 下雨"
|
wx_summary = (
|
||||||
|
"🌧️ 小雨" if {"-RA", "-DZ", "DZ"} & wx_tokens else "🌧️ 下雨"
|
||||||
|
)
|
||||||
elif snow_codes & wx_tokens:
|
elif snow_codes & wx_tokens:
|
||||||
wx_summary = "❄️ 下雪"
|
wx_summary = "❄️ 下雪"
|
||||||
elif fog_codes & wx_tokens:
|
elif fog_codes & wx_tokens:
|
||||||
@@ -776,25 +973,44 @@ def start_bot():
|
|||||||
if metar_clouds:
|
if metar_clouds:
|
||||||
cover_code = metar_clouds[-1].get("cover", "")
|
cover_code = metar_clouds[-1].get("cover", "")
|
||||||
|
|
||||||
if cover_code in ("SKC", "CLR") or (cover_code == "" and mgm_cloud is not None and mgm_cloud <= 1):
|
if cover_code in ("SKC", "CLR") or (
|
||||||
|
cover_code == "" and mgm_cloud is not None and mgm_cloud <= 1
|
||||||
|
):
|
||||||
wx_summary = "☀️ 晴"
|
wx_summary = "☀️ 晴"
|
||||||
elif cover_code == "FEW" or (cover_code == "" and mgm_cloud is not None and mgm_cloud <= 2):
|
elif cover_code == "FEW" or (
|
||||||
|
cover_code == "" and mgm_cloud is not None and mgm_cloud <= 2
|
||||||
|
):
|
||||||
wx_summary = "🌤️ 晴间少云"
|
wx_summary = "🌤️ 晴间少云"
|
||||||
elif cover_code == "SCT" or (cover_code == "" and mgm_cloud is not None and mgm_cloud <= 4):
|
elif cover_code == "SCT" or (
|
||||||
|
cover_code == "" and mgm_cloud is not None and mgm_cloud <= 4
|
||||||
|
):
|
||||||
wx_summary = "⛅ 晴间多云"
|
wx_summary = "⛅ 晴间多云"
|
||||||
elif cover_code == "BKN" or (cover_code == "" and mgm_cloud is not None and mgm_cloud <= 6):
|
elif cover_code == "BKN" or (
|
||||||
|
cover_code == "" and mgm_cloud is not None and mgm_cloud <= 6
|
||||||
|
):
|
||||||
wx_summary = "🌥️ 多云"
|
wx_summary = "🌥️ 多云"
|
||||||
elif cover_code == "OVC" or (cover_code == "" and mgm_cloud is not None and mgm_cloud <= 8):
|
elif cover_code == "OVC" or (
|
||||||
|
cover_code == "" and mgm_cloud is not None and mgm_cloud <= 8
|
||||||
|
):
|
||||||
wx_summary = "☁️ 阴天"
|
wx_summary = "☁️ 阴天"
|
||||||
elif mgm_cloud is not None:
|
elif mgm_cloud is not None:
|
||||||
cloud_names = {0: "☀️ 晴", 1: "🌤️ 晴", 2: "🌤️ 少云", 3: "⛅ 散云", 4: "⛅ 散云", 5: "🌥️ 多云", 6: "🌥️ 多云", 7: "☁️ 阴", 8: "☁️ 阴天"}
|
cloud_names = {
|
||||||
|
0: "☀️ 晴",
|
||||||
|
1: "🌤️ 晴",
|
||||||
|
2: "🌤️ 少云",
|
||||||
|
3: "⛅ 散云",
|
||||||
|
4: "⛅ 散云",
|
||||||
|
5: "🌥️ 多云",
|
||||||
|
6: "🌥️ 多云",
|
||||||
|
7: "☁️ 阴",
|
||||||
|
8: "☁️ 阴天",
|
||||||
|
}
|
||||||
wx_summary = cloud_names.get(mgm_cloud, "")
|
wx_summary = cloud_names.get(mgm_cloud, "")
|
||||||
|
|
||||||
wx_display = f" {wx_summary}" if wx_summary else ""
|
wx_display = f" {wx_summary}" if wx_summary else ""
|
||||||
msg_lines.append(f"\n✈️ <b>实测 ({main_source}): {cur_temp}{temp_symbol}</b>{max_str} |{wx_display} | {obs_t_str}{age_tag}")
|
msg_lines.append(
|
||||||
|
f"\n✈️ <b>实测 ({main_source}): {cur_temp}{temp_symbol}</b>{max_str} |{wx_display} | {obs_t_str}{age_tag}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if mgm:
|
if mgm:
|
||||||
m_c = mgm.get("current", {})
|
m_c = mgm.get("current", {})
|
||||||
@@ -811,13 +1027,17 @@ def start_bot():
|
|||||||
humidity = m_c.get("humidity")
|
humidity = m_c.get("humidity")
|
||||||
if feels_like is not None or humidity is not None:
|
if feels_like is not None or humidity is not None:
|
||||||
parts = []
|
parts = []
|
||||||
if feels_like is not None: parts.append(f"🌡️ 体感: {feels_like}°C")
|
if feels_like is not None:
|
||||||
if humidity is not None: parts.append(f"💧 {humidity}%")
|
parts.append(f"🌡️ 体感: {feels_like}°C")
|
||||||
|
if humidity is not None:
|
||||||
|
parts.append(f"💧 {humidity}%")
|
||||||
msg_lines.append(f" [MGM] {' | '.join(parts)}")
|
msg_lines.append(f" [MGM] {' | '.join(parts)}")
|
||||||
|
|
||||||
# 风况(跳过缺失数据)
|
# 风况(跳过缺失数据)
|
||||||
if wind_dir is not None and wind_speed_ms is not None:
|
if wind_dir is not None and wind_speed_ms is not None:
|
||||||
msg_lines.append(f" [MGM] 🌬️ {dir_str}{wind_dir}° ({wind_speed_ms} m/s) | 💧 降水: {m_c.get('rain_24h') or 0}mm")
|
msg_lines.append(
|
||||||
|
f" [MGM] 🌬️ {dir_str}{wind_dir}° ({wind_speed_ms} m/s) | 💧 降水: {m_c.get('rain_24h') or 0}mm"
|
||||||
|
)
|
||||||
|
|
||||||
# 新增:气压和云量
|
# 新增:气压和云量
|
||||||
extra_parts = []
|
extra_parts = []
|
||||||
@@ -826,7 +1046,17 @@ def start_bot():
|
|||||||
extra_parts.append(f"🌡 气压: {pressure}hPa")
|
extra_parts.append(f"🌡 气压: {pressure}hPa")
|
||||||
cloud_cover = m_c.get("cloud_cover")
|
cloud_cover = m_c.get("cloud_cover")
|
||||||
if cloud_cover is not None:
|
if cloud_cover is not None:
|
||||||
cloud_desc_map = {0: "晴朗", 1: "少云", 2: "少云", 3: "散云", 4: "散云", 5: "多云", 6: "多云", 7: "很多云", 8: "阴天"}
|
cloud_desc_map = {
|
||||||
|
0: "晴朗",
|
||||||
|
1: "少云",
|
||||||
|
2: "少云",
|
||||||
|
3: "散云",
|
||||||
|
4: "散云",
|
||||||
|
5: "多云",
|
||||||
|
6: "多云",
|
||||||
|
7: "很多云",
|
||||||
|
8: "阴天",
|
||||||
|
}
|
||||||
cloud_text = cloud_desc_map.get(cloud_cover, f"{cloud_cover}/8")
|
cloud_text = cloud_desc_map.get(cloud_cover, f"{cloud_cover}/8")
|
||||||
extra_parts.append(f"☁️ 云量: {cloud_text}({cloud_cover}/8)")
|
extra_parts.append(f"☁️ 云量: {cloud_text}({cloud_cover}/8)")
|
||||||
mgm_max = m_c.get("mgm_max_temp")
|
mgm_max = m_c.get("mgm_max_temp")
|
||||||
@@ -844,23 +1074,36 @@ def start_bot():
|
|||||||
|
|
||||||
cloud_desc = ""
|
cloud_desc = ""
|
||||||
if clouds:
|
if clouds:
|
||||||
c_map = {"BKN": "多云", "OVC": "阴天", "FEW": "少云", "SCT": "散云", "SKC": "晴", "CLR": "晴"}
|
c_map = {
|
||||||
|
"BKN": "多云",
|
||||||
|
"OVC": "阴天",
|
||||||
|
"FEW": "少云",
|
||||||
|
"SCT": "散云",
|
||||||
|
"SKC": "晴",
|
||||||
|
"CLR": "晴",
|
||||||
|
}
|
||||||
main = clouds[-1]
|
main = clouds[-1]
|
||||||
cloud_desc = f"☁️ {c_map.get(main.get('cover'), main.get('cover'))}"
|
cloud_desc = f"☁️ {c_map.get(main.get('cover'), main.get('cover'))}"
|
||||||
|
|
||||||
prefix = "[METAR]" if mgm else " "
|
prefix = "[METAR]" if mgm else " "
|
||||||
if not mgm:
|
if not mgm:
|
||||||
msg_lines.append(f" {prefix} 💨 {wind or 0}kt ({wind_dir or 0}°) | 👁️ {vis or 10}mi")
|
msg_lines.append(
|
||||||
|
f" {prefix} 💨 {wind or 0}kt ({wind_dir or 0}°) | 👁️ {vis or 10}mi"
|
||||||
|
)
|
||||||
|
|
||||||
if cloud_desc:
|
if cloud_desc:
|
||||||
msg_lines.append(f" {prefix} {cloud_desc} | 👁️ {vis or 10}mi | 💨 {wind or 0}kt")
|
msg_lines.append(
|
||||||
|
f" {prefix} {cloud_desc} | 👁️ {vis or 10}mi | 💨 {wind or 0}kt"
|
||||||
|
)
|
||||||
|
|
||||||
# --- 5. 态势特征提取 ---
|
# --- 5. 态势特征提取 ---
|
||||||
feature_str, ai_context = analyze_weather_trend(weather_data, temp_symbol, city_name)
|
feature_str, ai_context = analyze_weather_trend(
|
||||||
|
weather_data, temp_symbol, city_name
|
||||||
|
)
|
||||||
if feature_str:
|
if feature_str:
|
||||||
# 仅将最核心的信息展示给用户作为"态势分析"
|
# 仅将最核心的信息展示给用户作为"态势分析"
|
||||||
# 但后面会把更全的数据传给 AI
|
# 但后面会把更全的数据传给 AI
|
||||||
msg_lines.append(f"\n💡 <b>分析</b>:")
|
msg_lines.append("\n💡 <b>分析</b>:")
|
||||||
for line in feature_str.split("\n"):
|
for line in feature_str.split("\n"):
|
||||||
if line.strip():
|
if line.strip():
|
||||||
msg_lines.append(f"- {line.strip()}")
|
msg_lines.append(f"- {line.strip()}")
|
||||||
@@ -873,7 +1116,13 @@ def start_bot():
|
|||||||
# 补充多模型分歧
|
# 补充多模型分歧
|
||||||
mm = weather_data.get("multi_model", {})
|
mm = weather_data.get("multi_model", {})
|
||||||
if mm.get("forecasts"):
|
if mm.get("forecasts"):
|
||||||
mm_str = " | ".join([f"{k}:{v}{temp_symbol}" for k,v in mm["forecasts"].items() if v])
|
mm_str = " | ".join(
|
||||||
|
[
|
||||||
|
f"{k}:{v}{temp_symbol}"
|
||||||
|
for k, v in mm["forecasts"].items()
|
||||||
|
if v
|
||||||
|
]
|
||||||
|
)
|
||||||
ai_context += f"\n模型分歧: {mm_str}"
|
ai_context += f"\n模型分歧: {mm_str}"
|
||||||
|
|
||||||
ai_result = get_ai_analysis(ai_context, city_name, temp_symbol)
|
ai_result = get_ai_analysis(ai_context, city_name, temp_symbol)
|
||||||
@@ -886,11 +1135,13 @@ def start_bot():
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
logger.error(f"查询失败: {e}\n{traceback.format_exc()}")
|
logger.error(f"查询失败: {e}\n{traceback.format_exc()}")
|
||||||
bot.reply_to(message, f"❌ 查询失败: {e}")
|
bot.reply_to(message, f"❌ 查询失败: {e}")
|
||||||
|
|
||||||
logger.info("🤖 Bot 启动中...")
|
logger.info("🤖 Bot 启动中...")
|
||||||
bot.infinity_polling()
|
bot.infinity_polling()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
start_bot()
|
start_bot()
|
||||||
|
|||||||
+12
-10
@@ -9,6 +9,7 @@ MODELS = [
|
|||||||
"llama-3.1-8b-instant",
|
"llama-3.1-8b-instant",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def get_ai_analysis(weather_insights: str, city_name: str, temp_symbol: str) -> str:
|
def get_ai_analysis(weather_insights: str, city_name: str, temp_symbol: str) -> str:
|
||||||
"""
|
"""
|
||||||
通过 Groq API (LLaMA 3.3 70B) 对天气态势进行极速交易分析
|
通过 Groq API (LLaMA 3.3 70B) 对天气态势进行极速交易分析
|
||||||
@@ -20,10 +21,7 @@ def get_ai_analysis(weather_insights: str, city_name: str, temp_symbol: str) ->
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
url = "https://api.groq.com/openai/v1/chat/completions"
|
url = "https://api.groq.com/openai/v1/chat/completions"
|
||||||
headers = {
|
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
||||||
"Authorization": f"Bearer {api_key}",
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
|
|
||||||
prompt = f"""
|
prompt = f"""
|
||||||
你是一个专业的天气衍生品(如 Polymarket)交易员。你的任务是分析当前天气特征,判断今日实测最高温是否能达到或超过预报中的【最高值】。
|
你是一个专业的天气衍生品(如 Polymarket)交易员。你的任务是分析当前天气特征,判断今日实测最高温是否能达到或超过预报中的【最高值】。
|
||||||
@@ -69,18 +67,21 @@ P4 **预报背景**(最低优先级):
|
|||||||
payload = {
|
payload = {
|
||||||
"model": model,
|
"model": model,
|
||||||
"messages": [
|
"messages": [
|
||||||
{"role": "system", "content": "你是不讲废话、只看数据的专业气象分析师。"},
|
{
|
||||||
{"role": "user", "content": prompt}
|
"role": "system",
|
||||||
|
"content": "你是不讲废话、只看数据的专业气象分析师。",
|
||||||
|
},
|
||||||
|
{"role": "user", "content": prompt},
|
||||||
],
|
],
|
||||||
"temperature": 0.5,
|
"temperature": 0.5,
|
||||||
"max_tokens": 250
|
"max_tokens": 250,
|
||||||
}
|
}
|
||||||
|
|
||||||
response = requests.post(url, json=payload, headers=headers, timeout=15)
|
response = requests.post(url, json=payload, headers=headers, timeout=15)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
result = response.json()
|
result = response.json()
|
||||||
content = result['choices'][0]['message']['content'].strip()
|
content = result["choices"][0]["message"]["content"].strip()
|
||||||
|
|
||||||
if model != MODELS[0]:
|
if model != MODELS[0]:
|
||||||
logger.info(f"Groq 降级到备用模型 {model} 成功")
|
logger.info(f"Groq 降级到备用模型 {model} 成功")
|
||||||
@@ -93,7 +94,9 @@ P4 **预报背景**(最低优先级):
|
|||||||
time.sleep(1.5)
|
time.sleep(1.5)
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
logger.warning(f"Groq {model} 失败 (HTTP {status}),尝试下一个模型...")
|
logger.warning(
|
||||||
|
f"Groq {model} 失败 (HTTP {status}),尝试下一个模型..."
|
||||||
|
)
|
||||||
break # 换下一个模型
|
break # 换下一个模型
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Groq {model} 异常: {e},尝试下一个模型...")
|
logger.warning(f"Groq {model} 异常: {e},尝试下一个模型...")
|
||||||
@@ -101,4 +104,3 @@ P4 **预报背景**(最低优先级):
|
|||||||
|
|
||||||
logger.error("所有 Groq 模型均不可用")
|
logger.error("所有 Groq 模型均不可用")
|
||||||
return "\n⚠️ Groq AI 暂时不可用,请稍后再试"
|
return "\n⚠️ Groq AI 暂时不可用,请稍后再试"
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import logging
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
import fcntl
|
import fcntl
|
||||||
@@ -9,6 +8,7 @@ import fcntl
|
|||||||
_history_cache = {}
|
_history_cache = {}
|
||||||
_history_mtime = 0
|
_history_mtime = 0
|
||||||
|
|
||||||
|
|
||||||
def load_history(filepath):
|
def load_history(filepath):
|
||||||
global _history_cache, _history_mtime
|
global _history_cache, _history_mtime
|
||||||
if not os.path.exists(filepath):
|
if not os.path.exists(filepath):
|
||||||
@@ -19,7 +19,7 @@ def load_history(filepath):
|
|||||||
if current_mtime == _history_mtime and _history_cache:
|
if current_mtime == _history_mtime and _history_cache:
|
||||||
return _history_cache
|
return _history_cache
|
||||||
|
|
||||||
with open(filepath, 'r', encoding='utf-8') as f:
|
with open(filepath, "r", encoding="utf-8") as f:
|
||||||
# We don't strictly need a lock for reading in Python if the write is atomic,
|
# We don't strictly need a lock for reading in Python if the write is atomic,
|
||||||
# but using one prevents reading half-written JSONs.
|
# but using one prevents reading half-written JSONs.
|
||||||
fcntl.flock(f, fcntl.LOCK_SH)
|
fcntl.flock(f, fcntl.LOCK_SH)
|
||||||
@@ -33,11 +33,12 @@ def load_history(filepath):
|
|||||||
print(f"Error loading history: {e}")
|
print(f"Error loading history: {e}")
|
||||||
return _history_cache if _history_cache else {}
|
return _history_cache if _history_cache else {}
|
||||||
|
|
||||||
|
|
||||||
def save_history(filepath, data):
|
def save_history(filepath, data):
|
||||||
global _history_cache, _history_mtime
|
global _history_cache, _history_mtime
|
||||||
_history_cache = data
|
_history_cache = data
|
||||||
try:
|
try:
|
||||||
with open(filepath, 'w', encoding='utf-8') as f:
|
with open(filepath, "w", encoding="utf-8") as f:
|
||||||
fcntl.flock(f, fcntl.LOCK_EX)
|
fcntl.flock(f, fcntl.LOCK_EX)
|
||||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||||
fcntl.flock(f, fcntl.LOCK_UN)
|
fcntl.flock(f, fcntl.LOCK_UN)
|
||||||
@@ -45,15 +46,20 @@ def save_history(filepath, data):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error saving history: {e}")
|
print(f"Error saving history: {e}")
|
||||||
|
|
||||||
def update_daily_record(city_name, date_str, forecasts, actual_high, deb_prediction=None):
|
|
||||||
|
def update_daily_record(
|
||||||
|
city_name, date_str, forecasts, actual_high, deb_prediction=None
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
保存/更新某城市某天的各个模型预报与最终实测值
|
保存/更新某城市某天的各个模型预报与最终实测值
|
||||||
forecasts: dict, 例如 {"ECMWF": 28.5, "GFS": 30.0, ...}
|
forecasts: dict, 例如 {"ECMWF": 28.5, "GFS": 30.0, ...}
|
||||||
actual_high: float, 最终实测最高温
|
actual_high: float, 最终实测最高温
|
||||||
deb_prediction: float, DEB 融合预测值(用于准确率追踪)
|
deb_prediction: float, DEB 融合预测值(用于准确率追踪)
|
||||||
"""
|
"""
|
||||||
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
project_root = os.path.dirname(
|
||||||
history_file = os.path.join(project_root, 'data', 'daily_records.json')
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
)
|
||||||
|
history_file = os.path.join(project_root, "data", "daily_records.json")
|
||||||
|
|
||||||
data = load_history(history_file)
|
data = load_history(history_file)
|
||||||
if city_name not in data:
|
if city_name not in data:
|
||||||
@@ -63,14 +69,17 @@ def update_daily_record(city_name, date_str, forecasts, actual_high, deb_predict
|
|||||||
data[city_name][date_str] = {}
|
data[city_name][date_str] = {}
|
||||||
|
|
||||||
# 避免无意义的频繁磁盘写入
|
# 避免无意义的频繁磁盘写入
|
||||||
old_actual = data[city_name][date_str].get('actual_high')
|
old_actual = data[city_name][date_str].get("actual_high")
|
||||||
if old_actual == actual_high and data[city_name][date_str].get('forecasts') == forecasts:
|
if (
|
||||||
|
old_actual == actual_high
|
||||||
|
and data[city_name][date_str].get("forecasts") == forecasts
|
||||||
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
data[city_name][date_str]['forecasts'] = forecasts
|
data[city_name][date_str]["forecasts"] = forecasts
|
||||||
data[city_name][date_str]['actual_high'] = actual_high
|
data[city_name][date_str]["actual_high"] = actual_high
|
||||||
if deb_prediction is not None:
|
if deb_prediction is not None:
|
||||||
data[city_name][date_str]['deb_prediction'] = deb_prediction
|
data[city_name][date_str]["deb_prediction"] = deb_prediction
|
||||||
|
|
||||||
# 自动清理:只保留最近 14 天的记录(DEB 只用 7 天,14 天留足余量)
|
# 自动清理:只保留最近 14 天的记录(DEB 只用 7 天,14 天留足余量)
|
||||||
cutoff = (datetime.now() - timedelta(days=14)).strftime("%Y-%m-%d")
|
cutoff = (datetime.now() - timedelta(days=14)).strftime("%Y-%m-%d")
|
||||||
@@ -81,20 +90,24 @@ def update_daily_record(city_name, date_str, forecasts, actual_high, deb_predict
|
|||||||
|
|
||||||
save_history(history_file, data)
|
save_history(history_file, data)
|
||||||
|
|
||||||
|
|
||||||
def calculate_dynamic_weights(city_name, current_forecasts, lookback_days=7):
|
def calculate_dynamic_weights(city_name, current_forecasts, lookback_days=7):
|
||||||
"""
|
"""
|
||||||
计算动态权重融合 (Dynamic Ensemble Blending, DEB)
|
计算动态权重融合 (Dynamic Ensemble Blending, DEB)
|
||||||
根据过去 N 天各模型的 Mean Absolute Error (MAE) 计算倒数权重
|
根据过去 N 天各模型的 Mean Absolute Error (MAE) 计算倒数权重
|
||||||
返回: blended_high (融合预报值), weights_info (权重展示字符串)
|
返回: blended_high (融合预报值), weights_info (权重展示字符串)
|
||||||
"""
|
"""
|
||||||
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
project_root = os.path.dirname(
|
||||||
history_file = os.path.join(project_root, 'data', 'daily_records.json')
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
)
|
||||||
|
history_file = os.path.join(project_root, "data", "daily_records.json")
|
||||||
data = load_history(history_file)
|
data = load_history(history_file)
|
||||||
|
|
||||||
if city_name not in data or not data[city_name]:
|
if city_name not in data or not data[city_name]:
|
||||||
# 没有历史数据,返回简单的平均/中位数
|
# 没有历史数据,返回简单的平均/中位数
|
||||||
valid_vals = [v for v in current_forecasts.values() if v is not None]
|
valid_vals = [v for v in current_forecasts.values() if v is not None]
|
||||||
if not valid_vals: return None, "暂无模型数据"
|
if not valid_vals:
|
||||||
|
return None, "暂无模型数据"
|
||||||
avg = sum(valid_vals) / len(valid_vals)
|
avg = sum(valid_vals) / len(valid_vals)
|
||||||
return round(avg, 1), "等权平均(历史数据不足)"
|
return round(avg, 1), "等权平均(历史数据不足)"
|
||||||
|
|
||||||
@@ -113,8 +126,8 @@ def calculate_dynamic_weights(city_name, current_forecasts, lookback_days=7):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
record = city_data[date_str]
|
record = city_data[date_str]
|
||||||
actual = record.get('actual_high')
|
actual = record.get("actual_high")
|
||||||
past_forecasts = record.get('forecasts', {})
|
past_forecasts = record.get("forecasts", {})
|
||||||
|
|
||||||
if actual is None:
|
if actual is None:
|
||||||
continue
|
continue
|
||||||
@@ -143,7 +156,11 @@ def calculate_dynamic_weights(city_name, current_forecasts, lookback_days=7):
|
|||||||
maes[model] = 2.0
|
maes[model] = 2.0
|
||||||
|
|
||||||
# 计算权重(用 MAE 的倒数,误差越小权重越大;加 0.1 防止除以0)
|
# 计算权重(用 MAE 的倒数,误差越小权重越大;加 0.1 防止除以0)
|
||||||
inverse_errors = {m: 1.0 / (mae + 0.1) for m, mae in maes.items() if current_forecasts.get(m) is not None}
|
inverse_errors = {
|
||||||
|
m: 1.0 / (mae + 0.1)
|
||||||
|
for m, mae in maes.items()
|
||||||
|
if current_forecasts.get(m) is not None
|
||||||
|
}
|
||||||
|
|
||||||
total_inv = sum(inverse_errors.values())
|
total_inv = sum(inverse_errors.values())
|
||||||
if total_inv == 0:
|
if total_inv == 0:
|
||||||
@@ -160,7 +177,7 @@ def calculate_dynamic_weights(city_name, current_forecasts, lookback_days=7):
|
|||||||
sorted_models = sorted(weights.items(), key=lambda x: x[1], reverse=True)
|
sorted_models = sorted(weights.items(), key=lambda x: x[1], reverse=True)
|
||||||
weight_str_parts = []
|
weight_str_parts = []
|
||||||
for m, w in sorted_models[:3]:
|
for m, w in sorted_models[:3]:
|
||||||
weight_str_parts.append(f"{m}({w*100:.0f}%,MAE:{maes[m]:.1f}°)")
|
weight_str_parts.append(f"{m}({w * 100:.0f}%,MAE:{maes[m]:.1f}°)")
|
||||||
|
|
||||||
return round(blended_high, 1), " | ".join(weight_str_parts)
|
return round(blended_high, 1), " | ".join(weight_str_parts)
|
||||||
|
|
||||||
@@ -174,8 +191,10 @@ def get_deb_accuracy(city_name):
|
|||||||
- total_days: 有效天数
|
- total_days: 有效天数
|
||||||
- details_str: 格式化的展示字符串
|
- details_str: 格式化的展示字符串
|
||||||
"""
|
"""
|
||||||
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
project_root = os.path.dirname(
|
||||||
history_file = os.path.join(project_root, 'data', 'daily_records.json')
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
)
|
||||||
|
history_file = os.path.join(project_root, "data", "daily_records.json")
|
||||||
data = load_history(history_file)
|
data = load_history(history_file)
|
||||||
|
|
||||||
if city_name not in data:
|
if city_name not in data:
|
||||||
@@ -192,8 +211,8 @@ def get_deb_accuracy(city_name):
|
|||||||
if date_str == today_str:
|
if date_str == today_str:
|
||||||
continue # 跳过今天,还没结算
|
continue # 跳过今天,还没结算
|
||||||
record = city_data[date_str]
|
record = city_data[date_str]
|
||||||
deb_pred = record.get('deb_prediction')
|
deb_pred = record.get("deb_prediction")
|
||||||
actual = record.get('actual_high')
|
actual = record.get("actual_high")
|
||||||
|
|
||||||
if deb_pred is None or actual is None:
|
if deb_pred is None or actual is None:
|
||||||
continue
|
continue
|
||||||
@@ -201,7 +220,7 @@ def get_deb_accuracy(city_name):
|
|||||||
try:
|
try:
|
||||||
deb_pred = float(deb_pred)
|
deb_pred = float(deb_pred)
|
||||||
actual = float(actual)
|
actual = float(actual)
|
||||||
except:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
total += 1
|
total += 1
|
||||||
@@ -217,6 +236,8 @@ def get_deb_accuracy(city_name):
|
|||||||
hit_rate = hits / total * 100
|
hit_rate = hits / total * 100
|
||||||
mae = sum(errors) / len(errors)
|
mae = sum(errors) / len(errors)
|
||||||
|
|
||||||
details_str = f"过去{total}天 WU命中 {hits}/{total} ({hit_rate:.0f}%) | MAE: {mae:.1f}°"
|
details_str = (
|
||||||
|
f"过去{total}天 WU命中 {hits}/{total} ({hit_rate:.0f}%) | MAE: {mae:.1f}°"
|
||||||
|
)
|
||||||
|
|
||||||
return hit_rate, mae, total, details_str
|
return hit_rate, mae, total, details_str
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ CITY_RISK_PROFILES = {
|
|||||||
"warning": "冬天温差最不稳定",
|
"warning": "冬天温差最不稳定",
|
||||||
"season_notes": "冬季",
|
"season_notes": "冬季",
|
||||||
},
|
},
|
||||||
|
|
||||||
# 🟡 中危城市 - 存在系统偏差,需注意
|
# 🟡 中危城市 - 存在系统偏差,需注意
|
||||||
"ankara": {
|
"ankara": {
|
||||||
"risk_level": "medium",
|
"risk_level": "medium",
|
||||||
@@ -90,7 +89,6 @@ CITY_RISK_PROFILES = {
|
|||||||
"warning": "机场在北郊,冬季北风时比市区更冷",
|
"warning": "机场在北郊,冬季北风时比市区更冷",
|
||||||
"season_notes": "夏季热浪期间偏差最大",
|
"season_notes": "夏季热浪期间偏差最大",
|
||||||
},
|
},
|
||||||
|
|
||||||
# 🟢 低危城市 - 数据相对靠谱
|
# 🟢 低危城市 - 数据相对靠谱
|
||||||
"toronto": {
|
"toronto": {
|
||||||
"risk_level": "low",
|
"risk_level": "low",
|
||||||
@@ -183,11 +181,7 @@ def format_risk_warning(profile: dict, temp_symbol: str) -> str:
|
|||||||
lines = []
|
lines = []
|
||||||
|
|
||||||
# 风险等级标题
|
# 风险等级标题
|
||||||
risk_labels = {
|
risk_labels = {"high": "高危", "medium": "中危", "low": "低危"}
|
||||||
"high": "高危",
|
|
||||||
"medium": "中危",
|
|
||||||
"low": "低危"
|
|
||||||
}
|
|
||||||
risk_label = risk_labels.get(profile["risk_level"], "未知")
|
risk_label = risk_labels.get(profile["risk_level"], "未知")
|
||||||
lines.append(f"⚠️ <b>数据偏差风险</b>: {profile['risk_emoji']} {risk_label}")
|
lines.append(f"⚠️ <b>数据偏差风险</b>: {profile['risk_emoji']} {risk_label}")
|
||||||
|
|
||||||
|
|||||||
@@ -207,7 +207,9 @@ class WeatherDataCollector:
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def fetch_metar(self, city: str, use_fahrenheit: bool = False, utc_offset: int = 0) -> Optional[Dict]:
|
def fetch_metar(
|
||||||
|
self, city: str, use_fahrenheit: bool = False, utc_offset: int = 0
|
||||||
|
) -> Optional[Dict]:
|
||||||
"""
|
"""
|
||||||
从 NOAA Aviation Weather Center 获取 METAR 航空气象数据
|
从 NOAA Aviation Weather Center 获取 METAR 航空气象数据
|
||||||
|
|
||||||
@@ -239,7 +241,7 @@ class WeatherDataCollector:
|
|||||||
url,
|
url,
|
||||||
params=params,
|
params=params,
|
||||||
headers={"Cache-Control": "no-cache", "Pragma": "no-cache"},
|
headers={"Cache-Control": "no-cache", "Pragma": "no-cache"},
|
||||||
timeout=self.timeout
|
timeout=self.timeout,
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
@@ -258,39 +260,53 @@ class WeatherDataCollector:
|
|||||||
"""从 rawOb 中提取精确的 UTC 观测时间"""
|
"""从 rawOb 中提取精确的 UTC 观测时间"""
|
||||||
raw = obs.get("rawOb", "")
|
raw = obs.get("rawOb", "")
|
||||||
import re as _re
|
import re as _re
|
||||||
m = _re.search(r'\b(\d{2})(\d{2})(\d{2})Z\b', raw)
|
|
||||||
|
m = _re.search(r"\b(\d{2})(\d{2})(\d{2})Z\b", raw)
|
||||||
if m:
|
if m:
|
||||||
day, hour, minute = int(m.group(1)), int(m.group(2)), int(m.group(3))
|
_day, hour, minute = (
|
||||||
|
int(m.group(1)),
|
||||||
|
int(m.group(2)),
|
||||||
|
int(m.group(3)),
|
||||||
|
)
|
||||||
# 用 reportTime 的日期部分 + rawOb 的时分
|
# 用 reportTime 的日期部分 + rawOb 的时分
|
||||||
fallback = obs.get("reportTime", "")
|
fallback = obs.get("reportTime", "")
|
||||||
try:
|
try:
|
||||||
clean = fallback.replace(" ", "T")
|
clean = fallback.replace(" ", "T")
|
||||||
if not clean.endswith("Z"): clean += "Z"
|
if not clean.endswith("Z"):
|
||||||
|
clean += "Z"
|
||||||
base_dt = datetime.fromisoformat(clean.replace("Z", "+00:00"))
|
base_dt = datetime.fromisoformat(clean.replace("Z", "+00:00"))
|
||||||
result = base_dt.replace(hour=hour, minute=minute, second=0)
|
result = base_dt.replace(hour=hour, minute=minute, second=0)
|
||||||
# 处理跨日(如 rawOb 是23:50但 reportTime 已经是次日00:00)
|
# 处理跨日(如 rawOb 是23:50但 reportTime 已经是次日00:00)
|
||||||
if result > base_dt + timedelta(hours=2):
|
if result > base_dt + timedelta(hours=2):
|
||||||
result -= timedelta(days=1)
|
result -= timedelta(days=1)
|
||||||
return result
|
return result
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
# fallback 到 reportTime
|
# fallback 到 reportTime
|
||||||
fallback = obs.get("reportTime", "")
|
fallback = obs.get("reportTime", "")
|
||||||
try:
|
try:
|
||||||
clean = fallback.replace(" ", "T")
|
clean = fallback.replace(" ", "T")
|
||||||
if not clean.endswith("Z"): clean += "Z"
|
if not clean.endswith("Z"):
|
||||||
|
clean += "Z"
|
||||||
return datetime.fromisoformat(clean.replace("Z", "+00:00"))
|
return datetime.fromisoformat(clean.replace("Z", "+00:00"))
|
||||||
except:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
obs_dt = _parse_rawob_time(latest)
|
obs_dt = _parse_rawob_time(latest)
|
||||||
obs_time = obs_dt.strftime("%Y-%m-%dT%H:%M:%S.000Z") if obs_dt else latest.get("reportTime", "")
|
obs_time = (
|
||||||
|
obs_dt.strftime("%Y-%m-%dT%H:%M:%S.000Z")
|
||||||
|
if obs_dt
|
||||||
|
else latest.get("reportTime", "")
|
||||||
|
)
|
||||||
|
|
||||||
# 2. 精确计算"当地今天"的最高温
|
# 2. 精确计算"当地今天"的最高温
|
||||||
from datetime import timezone, timedelta
|
from datetime import timezone, timedelta
|
||||||
|
|
||||||
now_utc = datetime.now(timezone.utc)
|
now_utc = datetime.now(timezone.utc)
|
||||||
local_now = now_utc + timedelta(seconds=utc_offset)
|
local_now = now_utc + timedelta(seconds=utc_offset)
|
||||||
local_midnight = local_now.replace(hour=0, minute=0, second=0, microsecond=0)
|
local_midnight = local_now.replace(
|
||||||
|
hour=0, minute=0, second=0, microsecond=0
|
||||||
|
)
|
||||||
utc_midnight = local_midnight - timedelta(seconds=utc_offset)
|
utc_midnight = local_midnight - timedelta(seconds=utc_offset)
|
||||||
|
|
||||||
max_so_far_c = -999
|
max_so_far_c = -999
|
||||||
@@ -306,7 +322,7 @@ class WeatherDataCollector:
|
|||||||
max_so_far_c = t
|
max_so_far_c = t
|
||||||
local_report = obs_dt_iter + timedelta(seconds=utc_offset)
|
local_report = obs_dt_iter + timedelta(seconds=utc_offset)
|
||||||
max_temp_time = local_report.strftime("%H:%M")
|
max_temp_time = local_report.strftime("%H:%M")
|
||||||
except:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 3. 提取最近 4 条报文的多维数据(温度 + 风/云/压强,用于趋势和 shock_score)
|
# 3. 提取最近 4 条报文的多维数据(温度 + 风/云/压强,用于趋势和 shock_score)
|
||||||
@@ -319,21 +335,30 @@ class WeatherDataCollector:
|
|||||||
local_rt = obs_dt_iter + timedelta(seconds=utc_offset)
|
local_rt = obs_dt_iter + timedelta(seconds=utc_offset)
|
||||||
recent_temps_raw.append((local_rt.strftime("%H:%M"), obs_temp))
|
recent_temps_raw.append((local_rt.strftime("%H:%M"), obs_temp))
|
||||||
# 云量码映射: CLR=0, FEW=1, SCT=2, BKN=3, OVC=4
|
# 云量码映射: CLR=0, FEW=1, SCT=2, BKN=3, OVC=4
|
||||||
cloud_rank_map = {"CLR": 0, "SKC": 0, "FEW": 1, "SCT": 2, "BKN": 3, "OVC": 4}
|
cloud_rank_map = {
|
||||||
|
"CLR": 0,
|
||||||
|
"SKC": 0,
|
||||||
|
"FEW": 1,
|
||||||
|
"SCT": 2,
|
||||||
|
"BKN": 3,
|
||||||
|
"OVC": 4,
|
||||||
|
}
|
||||||
clouds = obs.get("clouds", [])
|
clouds = obs.get("clouds", [])
|
||||||
max_cloud_rank = 0
|
max_cloud_rank = 0
|
||||||
for c in clouds:
|
for c in clouds:
|
||||||
rank = cloud_rank_map.get(c.get("cover", ""), 0)
|
rank = cloud_rank_map.get(c.get("cover", ""), 0)
|
||||||
if rank > max_cloud_rank:
|
if rank > max_cloud_rank:
|
||||||
max_cloud_rank = rank
|
max_cloud_rank = rank
|
||||||
recent_obs_raw.append({
|
recent_obs_raw.append(
|
||||||
|
{
|
||||||
"time": local_rt.strftime("%H:%M"),
|
"time": local_rt.strftime("%H:%M"),
|
||||||
"temp": obs_temp,
|
"temp": obs_temp,
|
||||||
"wdir": obs.get("wdir"),
|
"wdir": obs.get("wdir"),
|
||||||
"wspd": obs.get("wspd"),
|
"wspd": obs.get("wspd"),
|
||||||
"cloud_rank": max_cloud_rank, # 0~4
|
"cloud_rank": max_cloud_rank, # 0~4
|
||||||
"altim": obs.get("altim"),
|
"altim": obs.get("altim"),
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# 转换为单位
|
# 转换为单位
|
||||||
if use_fahrenheit:
|
if use_fahrenheit:
|
||||||
@@ -342,7 +367,9 @@ class WeatherDataCollector:
|
|||||||
dewp = dewp_c * 9 / 5 + 32 if dewp_c is not None else None
|
dewp = dewp_c * 9 / 5 + 32 if dewp_c is not None else None
|
||||||
unit = "fahrenheit"
|
unit = "fahrenheit"
|
||||||
# 转换最近温度
|
# 转换最近温度
|
||||||
recent_temps = [(t, round(v * 9 / 5 + 32, 1)) for t, v in recent_temps_raw]
|
recent_temps = [
|
||||||
|
(t, round(v * 9 / 5 + 32, 1)) for t, v in recent_temps_raw
|
||||||
|
]
|
||||||
else:
|
else:
|
||||||
temp = temp_c
|
temp = temp_c
|
||||||
max_so_far = max_so_far_c if max_so_far_c > -900 else None
|
max_so_far = max_so_far_c if max_so_far_c > -900 else None
|
||||||
@@ -358,7 +385,9 @@ class WeatherDataCollector:
|
|||||||
"observation_time": obs_time,
|
"observation_time": obs_time,
|
||||||
"current": {
|
"current": {
|
||||||
"temp": round(temp, 1) if temp is not None else None,
|
"temp": round(temp, 1) if temp is not None else None,
|
||||||
"max_temp_so_far": round(max_so_far, 1) if max_so_far is not None else None,
|
"max_temp_so_far": round(max_so_far, 1)
|
||||||
|
if max_so_far is not None
|
||||||
|
else None,
|
||||||
"max_temp_time": max_temp_time,
|
"max_temp_time": max_temp_time,
|
||||||
"dewpoint": round(dewp, 1) if dewp is not None else None,
|
"dewpoint": round(dewp, 1) if dewp is not None else None,
|
||||||
"humidity": latest.get("rh"),
|
"humidity": latest.get("rh"),
|
||||||
@@ -396,17 +425,18 @@ class WeatherDataCollector:
|
|||||||
# 必须带 Origin,否则会被反爬拦截
|
# 必须带 Origin,否则会被反爬拦截
|
||||||
headers = {
|
headers = {
|
||||||
"Origin": "https://www.mgm.gov.tr",
|
"Origin": "https://www.mgm.gov.tr",
|
||||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||||
}
|
}
|
||||||
results = {}
|
results = {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 1. 实时数据 (添加时间戳防止 CDN 缓存)
|
# 1. 实时数据 (添加时间戳防止 CDN 缓存)
|
||||||
import time
|
import time
|
||||||
|
|
||||||
obs_resp = self.session.get(
|
obs_resp = self.session.get(
|
||||||
f"{base_url}/sondurumlar?istno={istno}&_={int(time.time()*1000)}",
|
f"{base_url}/sondurumlar?istno={istno}&_={int(time.time() * 1000)}",
|
||||||
headers=headers,
|
headers=headers,
|
||||||
timeout=self.timeout
|
timeout=self.timeout,
|
||||||
)
|
)
|
||||||
if obs_resp.status_code == 200:
|
if obs_resp.status_code == 200:
|
||||||
data = obs_resp.json()
|
data = obs_resp.json()
|
||||||
@@ -421,18 +451,39 @@ class WeatherDataCollector:
|
|||||||
return v is not None and v > -9000
|
return v is not None and v > -9000
|
||||||
|
|
||||||
results["current"] = {
|
results["current"] = {
|
||||||
"temp": latest.get("sicaklik") if _valid(latest.get("sicaklik")) else None,
|
"temp": latest.get("sicaklik")
|
||||||
"feels_like": latest.get("hissedilenSicaklik") if _valid(latest.get("hissedilenSicaklik")) else None,
|
if _valid(latest.get("sicaklik"))
|
||||||
"humidity": latest.get("nem") if _valid(latest.get("nem")) else None,
|
else None,
|
||||||
"wind_speed_ms": round(ruz_hiz_kmh / 3.6, 1) if _valid(ruz_hiz_kmh) else None,
|
"feels_like": latest.get("hissedilenSicaklik")
|
||||||
"wind_speed_kt": round(ruz_hiz_kmh / 1.852, 1) if _valid(ruz_hiz_kmh) else None,
|
if _valid(latest.get("hissedilenSicaklik"))
|
||||||
"wind_dir": latest.get("ruzgarYon") if _valid(latest.get("ruzgarYon")) else None,
|
else None,
|
||||||
"rain_24h": latest.get("toplamYagis") if _valid(latest.get("toplamYagis")) else None,
|
"humidity": latest.get("nem")
|
||||||
"pressure": latest.get("aktuelBasinc") if _valid(latest.get("aktuelBasinc")) else None,
|
if _valid(latest.get("nem"))
|
||||||
|
else None,
|
||||||
|
"wind_speed_ms": round(ruz_hiz_kmh / 3.6, 1)
|
||||||
|
if _valid(ruz_hiz_kmh)
|
||||||
|
else None,
|
||||||
|
"wind_speed_kt": round(ruz_hiz_kmh / 1.852, 1)
|
||||||
|
if _valid(ruz_hiz_kmh)
|
||||||
|
else None,
|
||||||
|
"wind_dir": latest.get("ruzgarYon")
|
||||||
|
if _valid(latest.get("ruzgarYon"))
|
||||||
|
else None,
|
||||||
|
"rain_24h": latest.get("toplamYagis")
|
||||||
|
if _valid(latest.get("toplamYagis"))
|
||||||
|
else None,
|
||||||
|
"pressure": latest.get("aktuelBasinc")
|
||||||
|
if _valid(latest.get("aktuelBasinc"))
|
||||||
|
else None,
|
||||||
"cloud_cover": latest.get("kapalilik"), # 0-8 八分位云量
|
"cloud_cover": latest.get("kapalilik"), # 0-8 八分位云量
|
||||||
"mgm_max_temp": latest.get("maxSicaklik") if _valid(latest.get("maxSicaklik")) else None,
|
"mgm_max_temp": latest.get("maxSicaklik")
|
||||||
|
if _valid(latest.get("maxSicaklik"))
|
||||||
|
else None,
|
||||||
"time": latest.get("veriZamani"),
|
"time": latest.get("veriZamani"),
|
||||||
"station_name": latest.get("istasyonAd") or latest.get("adi") or latest.get("merkezAd") or "Ankara Esenboğa"
|
"station_name": latest.get("istasyonAd")
|
||||||
|
or latest.get("adi")
|
||||||
|
or latest.get("merkezAd")
|
||||||
|
or "Ankara Esenboğa",
|
||||||
}
|
}
|
||||||
|
|
||||||
# 2. 每日预报(尝试两个可能的 API 路径)
|
# 2. 每日预报(尝试两个可能的 API 路径)
|
||||||
@@ -442,7 +493,9 @@ class WeatherDataCollector:
|
|||||||
]
|
]
|
||||||
for forecast_url in forecast_urls:
|
for forecast_url in forecast_urls:
|
||||||
try:
|
try:
|
||||||
daily_resp = self.session.get(forecast_url, headers=headers, timeout=self.timeout)
|
daily_resp = self.session.get(
|
||||||
|
forecast_url, headers=headers, timeout=self.timeout
|
||||||
|
)
|
||||||
if daily_resp.status_code == 200:
|
if daily_resp.status_code == 200:
|
||||||
forecasts = daily_resp.json()
|
forecasts = daily_resp.json()
|
||||||
if forecasts and isinstance(forecasts, list):
|
if forecasts and isinstance(forecasts, list):
|
||||||
@@ -452,14 +505,26 @@ class WeatherDataCollector:
|
|||||||
if high_val is not None:
|
if high_val is not None:
|
||||||
results["today_high"] = high_val
|
results["today_high"] = high_val
|
||||||
results["today_low"] = low_val
|
results["today_low"] = low_val
|
||||||
logger.info(f"📋 MGM 每日预报: 最高 {high_val}°C, 最低 {low_val}°C (from {forecast_url})")
|
logger.info(
|
||||||
|
f"📋 MGM 每日预报: 最高 {high_val}°C, 最低 {low_val}°C (from {forecast_url})"
|
||||||
|
)
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
# 记录所有可用字段,方便调试
|
# 记录所有可用字段,方便调试
|
||||||
available_keys = [k for k in today.keys() if "yuksek" in k.lower() or "sicaklik" in k.lower() or "gun" in k.lower()]
|
available_keys = [
|
||||||
logger.warning(f"MGM 每日预报: enYuksekGun1 为空,可用字段: {available_keys}")
|
k
|
||||||
|
for k in today.keys()
|
||||||
|
if "yuksek" in k.lower()
|
||||||
|
or "sicaklik" in k.lower()
|
||||||
|
or "gun" in k.lower()
|
||||||
|
]
|
||||||
|
logger.warning(
|
||||||
|
f"MGM 每日预报: enYuksekGun1 为空,可用字段: {available_keys}"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logger.debug(f"MGM forecast URL {forecast_url} returned {daily_resp.status_code}")
|
logger.debug(
|
||||||
|
f"MGM forecast URL {forecast_url} returned {daily_resp.status_code}"
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"MGM forecast URL {forecast_url} failed: {e}")
|
logger.debug(f"MGM forecast URL {forecast_url} failed: {e}")
|
||||||
|
|
||||||
@@ -478,7 +543,9 @@ class WeatherDataCollector:
|
|||||||
points_url = f"https://api.weather.gov/points/{lat},{lon}"
|
points_url = f"https://api.weather.gov/points/{lat},{lon}"
|
||||||
headers = {"User-Agent": "PolyWeather/1.0 (weather-bot)"}
|
headers = {"User-Agent": "PolyWeather/1.0 (weather-bot)"}
|
||||||
|
|
||||||
points_resp = self.session.get(points_url, headers=headers, timeout=self.timeout)
|
points_resp = self.session.get(
|
||||||
|
points_url, headers=headers, timeout=self.timeout
|
||||||
|
)
|
||||||
points_resp.raise_for_status()
|
points_resp.raise_for_status()
|
||||||
points_data = points_resp.json()
|
points_data = points_resp.json()
|
||||||
|
|
||||||
@@ -487,7 +554,9 @@ class WeatherDataCollector:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# 2. 获取预报
|
# 2. 获取预报
|
||||||
forecast_resp = self.session.get(forecast_url, headers=headers, timeout=self.timeout)
|
forecast_resp = self.session.get(
|
||||||
|
forecast_url, headers=headers, timeout=self.timeout
|
||||||
|
)
|
||||||
forecast_resp.raise_for_status()
|
forecast_resp.raise_for_status()
|
||||||
forecast_data = forecast_resp.json()
|
forecast_data = forecast_resp.json()
|
||||||
|
|
||||||
@@ -574,7 +643,7 @@ class WeatherDataCollector:
|
|||||||
# 记录今日模型分歧
|
# 记录今日模型分歧
|
||||||
daily_data["model_split"] = {
|
daily_data["model_split"] = {
|
||||||
"ecmwf": ecmwf_max[0] if ecmwf_max else None,
|
"ecmwf": ecmwf_max[0] if ecmwf_max else None,
|
||||||
"hrrr": hrrr_max[0] if hrrr_max else None
|
"hrrr": hrrr_max[0] if hrrr_max else None,
|
||||||
}
|
}
|
||||||
|
|
||||||
# 智能合并:HRRR 仅覆盖 48 小时,远期用 ECMWF 补全
|
# 智能合并:HRRR 仅覆盖 48 小时,远期用 ECMWF 补全
|
||||||
@@ -596,7 +665,9 @@ class WeatherDataCollector:
|
|||||||
# 映射逐小时数据
|
# 映射逐小时数据
|
||||||
hourly_data = data.get("hourly", {})
|
hourly_data = data.get("hourly", {})
|
||||||
if "temperature_2m_ncep_hrrr_conus" in hourly_data:
|
if "temperature_2m_ncep_hrrr_conus" in hourly_data:
|
||||||
hourly_data["temperature_2m"] = hourly_data["temperature_2m_ncep_hrrr_conus"]
|
hourly_data["temperature_2m"] = hourly_data[
|
||||||
|
"temperature_2m_ncep_hrrr_conus"
|
||||||
|
]
|
||||||
|
|
||||||
# 计算精确的当地时间
|
# 计算精确的当地时间
|
||||||
now_utc = datetime.utcnow()
|
now_utc = datetime.utcnow()
|
||||||
@@ -778,7 +849,9 @@ class WeatherDataCollector:
|
|||||||
forecasts = daily_forecasts.get(today_date, {})
|
forecasts = daily_forecasts.get(today_date, {})
|
||||||
|
|
||||||
labels_str = ", ".join([f"{k}={v}" for k, v in forecasts.items()])
|
labels_str = ", ".join([f"{k}={v}" for k, v in forecasts.items()])
|
||||||
logger.info(f"🔬 Multi-model ({len(forecasts)}个, {len(daily_forecasts)}天): {labels_str}")
|
logger.info(
|
||||||
|
f"🔬 Multi-model ({len(forecasts)}个, {len(daily_forecasts)}天): {labels_str}"
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"source": "multi_model",
|
"source": "multi_model",
|
||||||
@@ -814,14 +887,10 @@ class WeatherDataCollector:
|
|||||||
"lat": lat,
|
"lat": lat,
|
||||||
"lon": lon,
|
"lon": lon,
|
||||||
"format": "json",
|
"format": "json",
|
||||||
"as_daylight": "true"
|
"as_daylight": "true",
|
||||||
}
|
}
|
||||||
|
|
||||||
response = self.session.get(
|
response = self.session.get(url, params=params, timeout=self.timeout)
|
||||||
url,
|
|
||||||
params=params,
|
|
||||||
timeout=self.timeout
|
|
||||||
)
|
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
||||||
@@ -829,19 +898,21 @@ class WeatherDataCollector:
|
|||||||
max_temps = day_data.get("temperature_max", [])
|
max_temps = day_data.get("temperature_max", [])
|
||||||
|
|
||||||
if not max_temps:
|
if not max_temps:
|
||||||
logger.warning(f"Meteoblue API 返回数据中找不到最高温 (坐标: {lat},{lon})")
|
logger.warning(
|
||||||
|
f"Meteoblue API 返回数据中找不到最高温 (坐标: {lat},{lon})"
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# 2. 转换单位
|
# 2. 转换单位
|
||||||
def c_to_f(c):
|
def c_to_f(c):
|
||||||
return round((c * 9/5) + 32, 1)
|
return round((c * 9 / 5) + 32, 1)
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
"source": "meteoblue",
|
"source": "meteoblue",
|
||||||
"today_high": None,
|
"today_high": None,
|
||||||
"daily_highs": [],
|
"daily_highs": [],
|
||||||
"unit": "fahrenheit" if use_fahrenheit else "celsius",
|
"unit": "fahrenheit" if use_fahrenheit else "celsius",
|
||||||
"url": f"https://www.meteoblue.com/en/weather/week/{lat}N{lon}E" # 仅供参考
|
"url": f"https://www.meteoblue.com/en/weather/week/{lat}N{lon}E", # 仅供参考
|
||||||
}
|
}
|
||||||
|
|
||||||
# 提取今日最高
|
# 提取今日最高
|
||||||
@@ -854,7 +925,9 @@ class WeatherDataCollector:
|
|||||||
else:
|
else:
|
||||||
result["daily_highs"] = max_temps
|
result["daily_highs"] = max_temps
|
||||||
|
|
||||||
logger.info(f"✅ Meteoblue API 获取成功 ({lat},{lon}): 今天 {result['today_high']}{result['unit']}")
|
logger.info(
|
||||||
|
f"✅ Meteoblue API 获取成功 ({lat},{lon}): 今天 {result['today_high']}{result['unit']}"
|
||||||
|
)
|
||||||
return result
|
return result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Meteoblue API fetch failed: {e}")
|
logger.error(f"Meteoblue API fetch failed: {e}")
|
||||||
@@ -867,9 +940,18 @@ class WeatherDataCollector:
|
|||||||
"""
|
"""
|
||||||
# 1. 尝试英文月份
|
# 1. 尝试英文月份
|
||||||
months = {
|
months = {
|
||||||
"January": "01", "February": "02", "March": "03", "April": "04",
|
"January": "01",
|
||||||
"May": "05", "June": "06", "July": "07", "August": "08",
|
"February": "02",
|
||||||
"September": "09", "October": "10", "November": "11", "December": "12",
|
"March": "03",
|
||||||
|
"April": "04",
|
||||||
|
"May": "05",
|
||||||
|
"June": "06",
|
||||||
|
"July": "07",
|
||||||
|
"August": "08",
|
||||||
|
"September": "09",
|
||||||
|
"October": "10",
|
||||||
|
"November": "11",
|
||||||
|
"December": "12",
|
||||||
}
|
}
|
||||||
for month_name, month_val in months.items():
|
for month_name, month_val in months.items():
|
||||||
if month_name in title:
|
if month_name in title:
|
||||||
@@ -956,18 +1038,32 @@ class WeatherDataCollector:
|
|||||||
|
|
||||||
# 1. 优先尝试已知城市列表 (硬编码匹配)
|
# 1. 优先尝试已知城市列表 (硬编码匹配)
|
||||||
known_cities = {
|
known_cities = {
|
||||||
"london": "London", "伦敦": "London",
|
"london": "London",
|
||||||
"new york": "New York", "new york's central park": "New York", "nyc": "New York", "纽约": "New York",
|
"伦敦": "London",
|
||||||
"seattle": "Seattle", "西雅图": "Seattle",
|
"new york": "New York",
|
||||||
"chicago": "Chicago", "芝加哥": "Chicago",
|
"new york's central park": "New York",
|
||||||
"dallas": "Dallas", "达拉斯": "Dallas",
|
"nyc": "New York",
|
||||||
"miami": "Miami", "迈阿密": "Miami",
|
"纽约": "New York",
|
||||||
"atlanta": "Atlanta", "亚特兰大": "Atlanta",
|
"seattle": "Seattle",
|
||||||
"seoul": "Seoul", "首尔": "Seoul",
|
"西雅图": "Seattle",
|
||||||
"toronto": "Toronto", "多伦多": "Toronto",
|
"chicago": "Chicago",
|
||||||
"ankara": "Ankara", "安卡拉": "Ankara",
|
"芝加哥": "Chicago",
|
||||||
"wellington": "Wellington", "惠灵顿": "Wellington",
|
"dallas": "Dallas",
|
||||||
"buenos aires": "Buenos Aires", "布宜诺斯艾利斯": "Buenos Aires"
|
"达拉斯": "Dallas",
|
||||||
|
"miami": "Miami",
|
||||||
|
"迈阿密": "Miami",
|
||||||
|
"atlanta": "Atlanta",
|
||||||
|
"亚特兰大": "Atlanta",
|
||||||
|
"seoul": "Seoul",
|
||||||
|
"首尔": "Seoul",
|
||||||
|
"toronto": "Toronto",
|
||||||
|
"多伦多": "Toronto",
|
||||||
|
"ankara": "Ankara",
|
||||||
|
"安卡拉": "Ankara",
|
||||||
|
"wellington": "Wellington",
|
||||||
|
"惠灵顿": "Wellington",
|
||||||
|
"buenos aires": "Buenos Aires",
|
||||||
|
"布宜诺斯艾利斯": "Buenos Aires",
|
||||||
}
|
}
|
||||||
|
|
||||||
for key, val in known_cities.items():
|
for key, val in known_cities.items():
|
||||||
@@ -975,11 +1071,31 @@ class WeatherDataCollector:
|
|||||||
return val
|
return val
|
||||||
|
|
||||||
# 2. 从英文模板中提取
|
# 2. 从英文模板中提取
|
||||||
triggers = ["temperature in ", "temp in ", "weather in ", "highest-temperature-in-", "temperature-in-"]
|
triggers = [
|
||||||
|
"temperature in ",
|
||||||
|
"temp in ",
|
||||||
|
"weather in ",
|
||||||
|
"highest-temperature-in-",
|
||||||
|
"temperature-in-",
|
||||||
|
]
|
||||||
for trigger in triggers:
|
for trigger in triggers:
|
||||||
if trigger in q:
|
if trigger in q:
|
||||||
part = q.split(trigger)[1]
|
part = q.split(trigger)[1]
|
||||||
delimiters = [" on ", " at ", " above ", " below ", " be ", " is ", " will ", " has ", " reached ", "?", " (", ", ", "-"]
|
delimiters = [
|
||||||
|
" on ",
|
||||||
|
" at ",
|
||||||
|
" above ",
|
||||||
|
" below ",
|
||||||
|
" be ",
|
||||||
|
" is ",
|
||||||
|
" will ",
|
||||||
|
" has ",
|
||||||
|
" reached ",
|
||||||
|
"?",
|
||||||
|
" (",
|
||||||
|
", ",
|
||||||
|
"-",
|
||||||
|
]
|
||||||
city = part
|
city = part
|
||||||
for d in delimiters:
|
for d in delimiters:
|
||||||
if d in city:
|
if d in city:
|
||||||
@@ -1039,7 +1155,9 @@ class WeatherDataCollector:
|
|||||||
results["open-meteo"] = open_meteo
|
results["open-meteo"] = open_meteo
|
||||||
# 获取时区偏移以过滤 METAR
|
# 获取时区偏移以过滤 METAR
|
||||||
utc_offset = open_meteo.get("utc_offset", 0)
|
utc_offset = open_meteo.get("utc_offset", 0)
|
||||||
metar_data = self.fetch_metar(city, use_fahrenheit=use_fahrenheit, utc_offset=utc_offset)
|
metar_data = self.fetch_metar(
|
||||||
|
city, use_fahrenheit=use_fahrenheit, utc_offset=utc_offset
|
||||||
|
)
|
||||||
if metar_data:
|
if metar_data:
|
||||||
results["metar"] = metar_data
|
results["metar"] = metar_data
|
||||||
|
|
||||||
@@ -1052,9 +1170,10 @@ class WeatherDataCollector:
|
|||||||
# 对伦敦,获取 Meteoblue 预测 (公认最准)
|
# 对伦敦,获取 Meteoblue 预测 (公认最准)
|
||||||
if city_lower == "london":
|
if city_lower == "london":
|
||||||
mb_data = self.fetch_from_meteoblue(
|
mb_data = self.fetch_from_meteoblue(
|
||||||
lat, lon,
|
lat,
|
||||||
|
lon,
|
||||||
timezone_name=open_meteo.get("timezone", "UTC"),
|
timezone_name=open_meteo.get("timezone", "UTC"),
|
||||||
use_fahrenheit=use_fahrenheit
|
use_fahrenheit=use_fahrenheit,
|
||||||
)
|
)
|
||||||
if mb_data:
|
if mb_data:
|
||||||
results["meteoblue"] = mb_data
|
results["meteoblue"] = mb_data
|
||||||
@@ -1071,7 +1190,9 @@ class WeatherDataCollector:
|
|||||||
results["ensemble"] = ens_data
|
results["ensemble"] = ens_data
|
||||||
|
|
||||||
# 多模型预报 (所有城市通用,用于共识评分)
|
# 多模型预报 (所有城市通用,用于共识评分)
|
||||||
mm_data = self.fetch_multi_model(lat, lon, use_fahrenheit=use_fahrenheit)
|
mm_data = self.fetch_multi_model(
|
||||||
|
lat, lon, use_fahrenheit=use_fahrenheit
|
||||||
|
)
|
||||||
if mm_data:
|
if mm_data:
|
||||||
results["multi_model"] = mm_data
|
results["multi_model"] = mm_data
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -1,24 +1,25 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
# Set up logging
|
# Set up logging
|
||||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
logging.basicConfig(
|
||||||
|
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def fetch_historical_data_for_city(city_info, output_dir):
|
def fetch_historical_data_for_city(city_info, output_dir):
|
||||||
city_name = city_info['city']
|
city_name = city_info["city"]
|
||||||
lat = city_info['latitude']
|
lat = city_info["latitude"]
|
||||||
lon = city_info['longitude']
|
lon = city_info["longitude"]
|
||||||
|
|
||||||
# We will fetch data from Jan 1, 2023 to yesterday (or to latest available)
|
# We will fetch data from Jan 1, 2023 to yesterday (or to latest available)
|
||||||
start_date = "2023-01-01"
|
start_date = "2023-01-01"
|
||||||
end_date = "2025-12-31" # API handles dates in the future up to latest available archive usually
|
|
||||||
# For safety let's use a dynamic yesterday end_date
|
# For safety let's use a dynamic yesterday end_date
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
today = datetime.datetime.now()
|
today = datetime.datetime.now()
|
||||||
yesterday = (today - datetime.timedelta(days=2)).strftime("%Y-%m-%d")
|
yesterday = (today - datetime.timedelta(days=2)).strftime("%Y-%m-%d")
|
||||||
|
|
||||||
@@ -30,7 +31,9 @@ def fetch_historical_data_for_city(city_info, output_dir):
|
|||||||
"&timezone=auto"
|
"&timezone=auto"
|
||||||
)
|
)
|
||||||
|
|
||||||
logging.info(f"Downloading historical data for {city_name} (Lat: {lat}, Lon: {lon})...")
|
logging.info(
|
||||||
|
f"Downloading historical data for {city_name} (Lat: {lat}, Lon: {lon})..."
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.get(url, timeout=60)
|
response = requests.get(url, timeout=60)
|
||||||
@@ -47,33 +50,41 @@ def fetch_historical_data_for_city(city_info, output_dir):
|
|||||||
df = pd.DataFrame(hourly_data)
|
df = pd.DataFrame(hourly_data)
|
||||||
|
|
||||||
# Save to CSV
|
# Save to CSV
|
||||||
output_path = os.path.join(output_dir, f"{city_name.replace(' ', '_').lower()}_historical.csv")
|
output_path = os.path.join(
|
||||||
|
output_dir, f"{city_name.replace(' ', '_').lower()}_historical.csv"
|
||||||
|
)
|
||||||
df.to_csv(output_path, index=False)
|
df.to_csv(output_path, index=False)
|
||||||
|
|
||||||
logging.info(f"✅ Successfully saved historical data for {city_name} to {output_path}. Shape: {df.shape}")
|
logging.info(
|
||||||
|
f"✅ Successfully saved historical data for {city_name} to {output_path}. Shape: {df.shape}"
|
||||||
|
)
|
||||||
|
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
logging.error(f"❌ Network error while fetching data for {city_name}: {e}")
|
logging.error(f"❌ Network error while fetching data for {city_name}: {e}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"❌ Error processing data for {city_name}: {e}")
|
logging.error(f"❌ Error processing data for {city_name}: {e}")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
project_root = os.path.dirname(
|
||||||
config_path = os.path.join(project_root, 'config', 'config.yaml')
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
output_dir = os.path.join(project_root, 'data', 'historical')
|
)
|
||||||
|
config_path = os.path.join(project_root, "config", "config.yaml")
|
||||||
|
output_dir = os.path.join(project_root, "data", "historical")
|
||||||
|
|
||||||
os.makedirs(output_dir, exist_ok=True)
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
|
||||||
# Load config
|
# Load config
|
||||||
try:
|
try:
|
||||||
import yaml
|
import yaml
|
||||||
with open(config_path, 'r', encoding='utf-8') as f:
|
|
||||||
|
with open(config_path, "r", encoding="utf-8") as f:
|
||||||
config = yaml.safe_load(f)
|
config = yaml.safe_load(f)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Failed to load {config_path}: {e}")
|
logging.error(f"Failed to load {config_path}: {e}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
cities = config.get('cities', [])
|
cities = config.get("cities", [])
|
||||||
if not cities:
|
if not cities:
|
||||||
logging.warning("No cities found in config.yaml")
|
logging.warning("No cities found in config.yaml")
|
||||||
return
|
return
|
||||||
@@ -81,5 +92,6 @@ def main():
|
|||||||
for city_info in cities:
|
for city_info in cities:
|
||||||
fetch_historical_data_for_city(city_info, output_dir)
|
fetch_historical_data_for_city(city_info, output_dir)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
|
||||||
def load_config():
|
def load_config():
|
||||||
"""
|
"""
|
||||||
Load configuration from environment variables and config files
|
Load configuration from environment variables and config files
|
||||||
@@ -40,14 +41,14 @@ def load_config():
|
|||||||
"market_volume_signal": 0.15,
|
"market_volume_signal": 0.15,
|
||||||
"orderbook_analysis": 0.10,
|
"orderbook_analysis": 0.10,
|
||||||
"technical_indicators": 0.05,
|
"technical_indicators": 0.05,
|
||||||
"onchain_whale_signal": 0.05
|
"onchain_whale_signal": 0.05,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"log_level": os.getenv("LOG_LEVEL", "INFO"),
|
"log_level": os.getenv("LOG_LEVEL", "INFO"),
|
||||||
"env": os.getenv("ENV", "development"),
|
"env": os.getenv("ENV", "development"),
|
||||||
"proxy": os.getenv("HTTPS_PROXY") or os.getenv("HTTP_PROXY"),
|
"proxy": os.getenv("HTTPS_PROXY") or os.getenv("HTTP_PROXY"),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|||||||
+3
-2
@@ -1,6 +1,7 @@
|
|||||||
import sys
|
import sys
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
|
||||||
def setup_logger(level="DEBUG"):
|
def setup_logger(level="DEBUG"):
|
||||||
"""
|
"""
|
||||||
Configure loguru logger
|
Configure loguru logger
|
||||||
@@ -11,7 +12,7 @@ def setup_logger(level="DEBUG"):
|
|||||||
logger.add(
|
logger.add(
|
||||||
sys.stderr,
|
sys.stderr,
|
||||||
format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <level>{message}</level>",
|
format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <level>{message}</level>",
|
||||||
level=level
|
level=level,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 文件输出
|
# 文件输出
|
||||||
@@ -21,7 +22,7 @@ def setup_logger(level="DEBUG"):
|
|||||||
retention="10 days",
|
retention="10 days",
|
||||||
level=level,
|
level=level,
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
compression="zip"
|
compression="zip",
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info("日志系统初始化完成。")
|
logger.info("日志系统初始化完成。")
|
||||||
|
|||||||
Reference in New Issue
Block a user