fix(ci): format code and fix ruff lintings issues

This commit is contained in:
2569718930@qq.com
2026-03-03 00:12:16 +08:00
parent e423eaeb77
commit c8a7fe8548
8 changed files with 872 additions and 469 deletions
+393 -142
View File
@@ -1,7 +1,6 @@
import sys
import os
from datetime import datetime
from typing import List, Dict, Any, Optional
from typing import List
import telebot # 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:
sys.path.insert(0, project_root)
from src.utils.config_loader import load_config # type: ignore
from src.data_collection.weather_sources import WeatherDataCollector # type: ignore
from src.data_collection.city_risk_profiles import get_city_risk_profile, format_risk_warning # type: ignore
from src.analysis.deb_algorithm import calculate_dynamic_weights, update_daily_record
from src.utils.config_loader import load_config # type: ignore # noqa: E402
from src.data_collection.weather_sources import WeatherDataCollector # type: ignore # noqa: E402
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 # noqa: E402
def analyze_weather_trend(weather_data, temp_symbol, city_name=None):
'''根据实测与预测分析气温态势,增加峰值时刻预测'''
"""根据实测与预测分析气温态势,增加峰值时刻预测"""
insights: 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", {})
mb = weather_data.get("meteoblue", {})
nws = weather_data.get("nws", {})
mgm = weather_data.get("mgm", {})
weather_data.get("mgm", {})
if not metar or not open_meteo:
return "", ""
@@ -32,11 +32,14 @@ def analyze_weather_trend(weather_data, temp_symbol, city_name=None):
# 数值归一化:防止 JSON 反序列化后的 str 类型炸数学运算
def _sf(v):
"""safe float"""
if v is None: return None
try: return float(v)
except: return None
if v is None:
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"))
daily = open_meteo.get("daily", {})
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 主要计算最高预留。
forecast_highs = [h for h in current_forecasts.values() if h is not None]
forecast_high = max(forecast_highs) if forecast_highs else None
min_forecast_high = min(forecast_highs) if forecast_highs else forecast_high
forecast_median = sorted(forecast_highs)[len(forecast_highs) // 2] if forecast_highs else None
min(forecast_highs) if forecast_highs else forecast_high
forecast_median = (
sorted(forecast_highs)[len(forecast_highs) // 2] if forecast_highs else None
)
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(":")
local_hour = int(time_parts[0])
local_minute = int(time_parts[1]) if len(time_parts) > 1 else 0
except:
except Exception:
from datetime import datetime
local_date_str = datetime.now().strftime("%Y-%m-%d")
local_hour = datetime.now().hour
local_minute = datetime.now().minute
@@ -82,15 +88,28 @@ def analyze_weather_trend(weather_data, temp_symbol, city_name=None):
# === DEB 融合渲染 ===
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:
insights.insert(0, f"🧬 <b>DEB 融合预测</b><b>{blended_high}{temp_symbol}</b> ({weight_info})")
ai_features.append(f"🧬 DEB系统已通过历史偏差矫正算出期待点是: {blended_high}{temp_symbol}")
insights.insert(
0,
f"🧬 <b>DEB 融合预测</b><b>{blended_high}{temp_symbol}</b> ({weight_info})",
)
ai_features.append(
f"🧬 DEB系统已通过历史偏差矫正算出期待点是: {blended_high}{temp_symbol}"
)
# 顺便把今天的预测记录下来供之后回测用
try:
update_daily_record(city_name, local_date_str, current_forecasts, max_so_far, deb_prediction=blended_high)
except:
update_daily_record(
city_name,
local_date_str,
current_forecasts,
max_so_far,
deb_prediction=blended_high,
)
except Exception:
pass
# === METAR 趋势分析 (移到前部判断降温) ===
@@ -103,16 +122,31 @@ def analyze_weather_trend(weather_data, temp_symbol, city_name=None):
diff = latest_val - prev_val
if len(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_falling = all(temps_only[i] <= temps_only[i+1] for i in range(min(3, len(temps_only)) - 1))
trend_display = "".join([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})。"
all_rising = all(
temps_only[i] >= temps_only[i + 1]
for i in range(min(3, len(temps_only)) - 1)
)
all_falling = all(
temps_only[i] <= temps_only[i + 1]
for i in range(min(3, len(temps_only)) - 1)
)
trend_display = "".join(
[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
@@ -140,11 +174,14 @@ def analyze_weather_trend(weather_data, temp_symbol, city_name=None):
ens_median = _sf(ensemble.get("median"))
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}]。"
if not is_cooling: insights.append(msg1)
if not is_cooling:
insights.append(msg1)
ai_features.append(msg1)
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}"
ai_features.append(msg2)
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
# 用 P10/P90 反推标准差: P10 = median - 1.28*sigma, P90 = median + 1.28*sigma
sigma = (ens_p90 - ens_p10) / 2.56
if sigma < 0.1: sigma = 0.1 # 防止除以零
if sigma < 0.1:
sigma = 0.1 # 防止除以零
# 用 DEB 历史 MAE 作为 σ 的下限
# 如果模型过去的平均误差远大于集合预报的 σ,说明集合低估了真实不确定性
if city_name:
from src.analysis.deb_algorithm import get_deb_accuracy
acc = get_deb_accuracy(city_name)
if 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
if wdir_old is not None and wdir_new is not None:
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_shock = min(angle_diff / 90.0, 1.0) * wind_weight * 0.4
shock_score += wind_shock
@@ -207,7 +248,7 @@ def analyze_weather_trend(weather_data, temp_symbol, city_name=None):
# 应用 shock_score: 放宽 σ
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:
# (现有概率计算逻辑保留,但增加 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:
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
probs = {}
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)
if p > 0.01: probs[n] = p
if p > 0.01:
probs[n] = p
total_p = sum(probs.values())
if total_p > 0:
probs = {k: v / total_p for k, v in probs.items()}
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:
prob_str = " | ".join(prob_parts)
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"
dead_msg = f"🎲 <b>结算预测</b>:已锁定 {settled_wu}{temp_symbol} (死盘确认)"
insights.append(dead_msg)
ai_features.append(f"🎲 状态: 确认死盘,结算已无悬念。")
ai_features.append("🎲 状态: 确认死盘,结算已无悬念。")
# === 实测已超预报 & 趋势输出 ===
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
bt_msg = f"🚨 <b>实测已超预报</b>{max_so_far}{temp_symbol} 超过上限 {forecast_high}{temp_symbol}+{exceed_by:.1f}°)。"
insights.append(bt_msg)
ai_features.append(f"🚨 异常: 实测已冲破所有预报上限 ({max_so_far}{temp_symbol} vs {forecast_high}{temp_symbol})。")
if trend_desc: ai_features.append(trend_desc)
ai_features.append(
f"🚨 异常: 实测已冲破所有预报上限 ({max_so_far}{temp_symbol} vs {forecast_high}{temp_symbol})。"
)
if trend_desc:
ai_features.append(trend_desc)
else:
if trend_desc:
ai_features.append(trend_desc)
@@ -289,21 +342,39 @@ def analyze_weather_trend(weather_data, temp_symbol, city_name=None):
# === 峰值时刻 AI 提示 ===
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 last_peak_h < 6:
ai_features.append(f"⚠️ <b>提示</b>:预测最热在凌晨,后续气温可能一路走低。")
elif local_hour < first_peak_h and (max_so_far is None or max_so_far < forecast_high):
ai_features.append(
"⚠️ <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
ai_features.append(f"🎯 <b>关注重点</b>:看看那个时段能否涨到 {target_temp}{temp_symbol}")
ai_features.append(
f"🎯 <b>关注重点</b>:看看那个时段能否涨到 {target_temp}{temp_symbol}"
)
# 写给AI(使用精确到分钟的时间)
remain_hrs = first_peak_h - local_hour_frac
if local_hour_frac > last_peak_h: ai_features.append(f"⏱️ 状态: 预报峰值时段已过 ({window})。")
elif first_peak_h <= local_hour_frac <= last_peak_h: 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})。")
if local_hour_frac > last_peak_h:
ai_features.append(f"⏱️ 状态: 预报峰值时段已过 ({window})。")
elif first_peak_h <= local_hour_frac <= last_peak_h:
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 从趋势数据中误读
@@ -311,10 +382,13 @@ def analyze_weather_trend(weather_data, temp_symbol, city_name=None):
if current_temp is not None:
ai_features.append(f"🌡️ 当前实测温度: {current_temp}{temp_symbol}")
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
from src.data_collection.city_risk_profiles import get_city_risk_profile
if city_name:
_profile = get_city_risk_profile(city_name)
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", "未知")
ai_features.append(f"🌬️ 当下风况: 约 {wind_speed}kt (方向 {wind_dir}°)。")
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", [])
if clouds:
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}")
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", "")
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
hourly_rad = hourly.get("shortwave_radiation", [])
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
break
if max_temp_rad < 50:
ai_features.append(f"🌙 动力事实: 最高温出现在低辐射时段 ({max_temp_time_str}, 辐射{max_temp_rad:.0f}W/m²)。")
except: pass
ai_features.append(
f"🌙 动力事实: 最高温出现在低辐射时段 ({max_temp_time_str}, 辐射{max_temp_rad:.0f}W/m²)。"
)
except Exception:
pass
display_str = "\n".join(insights) if insights else ""
return display_str, "\n".join(ai_features)
def start_bot():
config = load_config()
token = os.getenv("TELEGRAM_BOT_TOKEN")
@@ -387,34 +472,47 @@ def start_bot():
try:
parts = message.text.split(maxsplit=1)
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
city_input = parts[1].strip().lower()
# 复用城市名映射
city_aliases = {
"ank": "ankara", "lon": "london", "par": "paris",
"nyc": "new york", "chi": "chicago", "dal": "dallas",
"mia": "miami", "atl": "atlanta", "sea": "seattle",
"tor": "toronto", "sel": "seoul", "ba": "buenos aires",
"ank": "ankara",
"lon": "london",
"par": "paris",
"nyc": "new york",
"chi": "chicago",
"dal": "dallas",
"mia": "miami",
"atl": "atlanta",
"sea": "seattle",
"tor": "toronto",
"sel": "seoul",
"ba": "buenos aires",
"wel": "wellington",
}
city_name = city_aliases.get(city_input, city_input)
from src.analysis.deb_algorithm import get_deb_accuracy, load_history
from src.analysis.deb_algorithm import load_history
import os as _os
# 获取详细历史数据
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)
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
city_data = data[city_name]
from datetime import datetime as _dt
today_str = _dt.now().strftime("%Y-%m-%d")
lines = [f"📊 <b>DEB 准确率报告 - {city_name.title()}</b>\n"]
@@ -429,20 +527,24 @@ def start_bot():
for date_str in sorted(city_data.keys()):
record = city_data[date_str]
actual = record.get('actual_high')
deb_pred = record.get('deb_prediction')
forecasts = record.get('forecasts', {})
actual = record.get("actual_high")
deb_pred = record.get("deb_prediction")
forecasts = record.get("forecasts", {})
if actual is None:
continue
try:
actual = float(actual)
if deb_pred is not None: deb_pred = float(deb_pred)
except: continue
if deb_pred is not None:
deb_pred = float(deb_pred)
except Exception:
continue
# 如果没有存 DEB 预测值,用当天各模型平均值回算
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:
deb_pred = round(sum(valid_preds) / len(valid_preds), 1)
@@ -453,18 +555,25 @@ def start_bot():
total_days += 1
deb_wu = round(deb_pred)
hit = deb_wu == actual_wu
if hit: hits += 1
if hit:
hits += 1
err = deb_pred - actual
deb_errors.append(abs(err))
signed_errors.append(err)
icon = "" if hit else ""
retro = "" if 'deb_prediction' not in record else ""
retro = "" if "deb_prediction" not in record else ""
# 错误类型标签
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:
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:
lines.append(f" {date_str}: 📍 今天进行中 (实测暂 {actual})")
@@ -480,12 +589,16 @@ def start_bot():
if total_days > 0:
hit_rate = hits / total_days * 100
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 对比
if model_errors:
lines.append(f"\n📈 <b>模型 MAE 对比</b>")
model_maes = {m: sum(e)/len(e) for m, e in model_errors.items() if e}
lines.append("\n📈 <b>模型 MAE 对比</b>")
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])
for m, mae in sorted_models:
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)
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:
bias_dir = "低估" if mean_bias < 0 else "高估"
lines.append(f" ⚠️ 系统性{bias_dir}:平均偏差 {mean_bias:+.1f}°")
else:
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:
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:
lines.append(f" 该城市模型集体高估趋势明显({mean_bias:+.1f}°),实际最高温可能比 DEB 融合值低。交易时建议适当看低。")
lines.append(
f" 该城市模型集体高估趋势明显({mean_bias:+.1f}°),实际最高温可能比 DEB 融合值低。交易时建议适当看低。"
)
elif deb_mae > 1.5:
lines.append(f" 该城市预报波动大 (MAE {deb_mae:.1f}°),建议观望或轻仓。")
lines.append(
f" 该城市预报波动大 (MAE {deb_mae:.1f}°),建议观望或轻仓。"
)
elif hit_rate >= 60:
lines.append(f" DEB 表现良好,可作为主要参考。")
lines.append(" DEB 表现良好,可作为主要参考。")
else:
lines.append(f" 数据积累中,建议结合 AI 分析综合判断。")
lines.append(" 数据积累中,建议结合 AI 分析综合判断。")
lines.append(f"\n📝 MAE = 平均绝对误差,越小越准。⭐ = 优于 DEB 融合。")
lines.append("\n📝 MAE = 平均绝对误差,越小越准。⭐ = 优于 DEB 融合。")
else:
lines.append("\n⏳ 尚无完整的 DEB 预测记录,明天起开始统计。")
@@ -544,20 +665,36 @@ def start_bot():
# --- 核心标准名称映射表 ---
# 这里的 Key 是缩写或别名,Value 是 Open-Meteo 识别的标准全称
STANDARD_MAPPING = {
"sel": "seoul", "seo": "seoul", "首尔": "seoul",
"lon": "london", "伦敦": "london",
"tor": "toronto", "多伦多": "toronto",
"ank": "ankara", "安卡拉": "ankara",
"wel": "wellington", "惠灵顿": "wellington",
"ba": "buenos aires", "布宜诺斯艾利斯": "buenos aires",
"nyc": "new york", "ny": "new york", "纽约": "new york",
"chi": "chicago", "芝加哥": "chicago",
"sea": "seattle", "西雅图": "seattle",
"mia": "miami", "迈阿密": "miami",
"atl": "atlanta", "亚特兰大": "atlanta",
"dal": "dallas", "达拉斯": "dallas",
"la": "los angeles", "洛杉矶": "los angeles",
"par": "paris", "巴黎": "paris",
"sel": "seoul",
"seo": "seoul",
"首尔": "seoul",
"lon": "london",
"伦敦": "london",
"tor": "toronto",
"多伦多": "toronto",
"ank": "ankara",
"安卡拉": "ankara",
"wel": "wellington",
"惠灵顿": "wellington",
"ba": "buenos aires",
"布宜诺斯艾利斯": "buenos aires",
"nyc": "new york",
"ny": "new york",
"纽约": "new york",
"chi": "chicago",
"芝加哥": "chicago",
"sea": "seattle",
"西雅图": "seattle",
"mia": "miami",
"迈阿密": "miami",
"atl": "atlanta",
"亚特兰大": "atlanta",
"dal": "dallas",
"达拉斯": "dallas",
"la": "los angeles",
"洛杉矶": "los angeles",
"par": "paris",
"巴黎": "paris",
}
# 支持的城市全名列表(用于模糊匹配)
@@ -596,23 +733,30 @@ def start_bot():
)
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)
if not coords:
bot.reply_to(message, f"❌ 未找到城市坐标: {city_name}")
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", {})
metar = weather_data.get("metar", {})
mgm = weather_data.get("mgm", {})
# 数值归一化
def _sf(v):
if v is None: return None
try: return float(v)
except: return None
if v is None:
return None
try:
return float(v)
except Exception:
return None
temp_unit = open_meteo.get("unit", "celsius")
temp_symbol = "°F" if temp_unit == "fahrenheit" else "°C"
@@ -630,7 +774,9 @@ def start_bot():
# --- 2. 紧凑 风险提示 ---
if risk_profile:
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. 紧凑 预测区 ---
daily = open_meteo.get("daily", {})
@@ -648,13 +794,25 @@ def start_bot():
if mb_high is not None:
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:
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:
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)
divergence_warning = ""
@@ -662,13 +820,17 @@ def start_bot():
diff = abs(mb_high - (_sf(max_temps[0]) or 0))
threshold = 5.0 if temp_unit == "fahrenheit" else 2.5
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 ""
sources_str = " | ".join(sources)
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:
@@ -682,8 +844,16 @@ def start_bot():
sunsets = daily.get("sunset", [])
sunshine_durations = daily.get("sunshine_duration", [])
if sunrises and sunsets:
sunrise_t = 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]
sunrise_t = (
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}"
if sunshine_durations:
sunshine_hours = sunshine_durations[0] / 3600 # 秒 -> 小时
@@ -692,9 +862,17 @@ def start_bot():
# --- 4. 核心 实测区 (合并 METAR 和 MGM) ---
# 基础数据优先用 METAR
cur_temp = _sf(metar.get("current", {}).get("temp") 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
cur_temp = _sf(
metar.get("current", {}).get("temp")
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"
metar_age_min = None # METAR 数据年龄(分钟)
main_source = "METAR" if metar else "MGM"
@@ -704,9 +882,12 @@ def start_bot():
try:
if "T" in obs_t:
from datetime import datetime, timezone, timedelta
dt = datetime.fromisoformat(obs_t.replace("Z", "+00:00"))
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")
# 计算数据年龄
now_utc = datetime.now(timezone.utc)
@@ -715,14 +896,17 @@ def start_bot():
obs_t_str = obs_t.split(" ")[1][:5]
else:
obs_t_str = obs_t
except:
except Exception:
obs_t_str = obs_t[:16]
elif mgm:
m_time = mgm.get("current", {}).get("time", "")
if "T" in m_time:
from datetime import datetime, timezone, timedelta
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:
m_time = m_time.split(" ")[1][:5]
obs_t_str = m_time
@@ -738,6 +922,7 @@ def start_bot():
max_str = ""
if max_p is not None:
import math
settled_val = math.floor(max_p + 0.5)
max_str = f" (最高: {max_p}{temp_symbol}"
if max_p_time:
@@ -754,7 +939,17 @@ def start_bot():
if metar_wx:
wx_upper = metar_wx.upper().strip()
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"}
fog_codes = {"FG", "BR", "HZ", "FZFG"}
ts_codes = {"TS", "TSRA"}
@@ -763,7 +958,9 @@ def start_bot():
elif {"+RA", "+SN"} & wx_tokens:
wx_summary = "🌧️ 大雨" if "+RA" in wx_tokens else "❄️ 大雪"
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:
wx_summary = "❄️ 下雪"
elif fog_codes & wx_tokens:
@@ -776,25 +973,44 @@ def start_bot():
if metar_clouds:
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 = "☀️ 晴"
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 = "🌤️ 晴间少云"
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 = "⛅ 晴间多云"
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 = "🌥️ 多云"
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 = "☁️ 阴天"
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_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:
m_c = mgm.get("current", {})
@@ -811,13 +1027,17 @@ def start_bot():
humidity = m_c.get("humidity")
if feels_like is not None or humidity is not None:
parts = []
if feels_like is not None: parts.append(f"🌡️ 体感: {feels_like}°C")
if humidity is not None: parts.append(f"💧 {humidity}%")
if feels_like is not None:
parts.append(f"🌡️ 体感: {feels_like}°C")
if humidity is not None:
parts.append(f"💧 {humidity}%")
msg_lines.append(f" [MGM] {' | '.join(parts)}")
# 风况(跳过缺失数据)
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 = []
@@ -826,7 +1046,17 @@ def start_bot():
extra_parts.append(f"🌡 气压: {pressure}hPa")
cloud_cover = m_c.get("cloud_cover")
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")
extra_parts.append(f"☁️ 云量: {cloud_text}({cloud_cover}/8)")
mgm_max = m_c.get("mgm_max_temp")
@@ -844,23 +1074,36 @@ def start_bot():
cloud_desc = ""
if clouds:
c_map = {"BKN": "多云", "OVC": "阴天", "FEW": "少云", "SCT": "散云", "SKC": "", "CLR": ""}
c_map = {
"BKN": "多云",
"OVC": "阴天",
"FEW": "少云",
"SCT": "散云",
"SKC": "",
"CLR": "",
}
main = clouds[-1]
cloud_desc = f"☁️ {c_map.get(main.get('cover'), main.get('cover'))}"
prefix = "[METAR]" if mgm else " "
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:
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. 态势特征提取 ---
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:
# 仅将最核心的信息展示给用户作为"态势分析"
# 但后面会把更全的数据传给 AI
msg_lines.append(f"\n💡 <b>分析</b>:")
msg_lines.append("\n💡 <b>分析</b>:")
for line in feature_str.split("\n"):
if line.strip():
msg_lines.append(f"- {line.strip()}")
@@ -873,7 +1116,13 @@ def start_bot():
# 补充多模型分歧
mm = weather_data.get("multi_model", {})
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_result = get_ai_analysis(ai_context, city_name, temp_symbol)
@@ -886,11 +1135,13 @@ def start_bot():
except Exception as e:
import traceback
logger.error(f"查询失败: {e}\n{traceback.format_exc()}")
bot.reply_to(message, f"❌ 查询失败: {e}")
logger.info("🤖 Bot 启动中...")
bot.infinity_polling()
if __name__ == "__main__":
start_bot()
+12 -10
View File
@@ -9,6 +9,7 @@ MODELS = [
"llama-3.1-8b-instant",
]
def get_ai_analysis(weather_insights: str, city_name: str, temp_symbol: str) -> str:
"""
通过 Groq API (LLaMA 3.3 70B) 对天气态势进行极速交易分析
@@ -20,10 +21,7 @@ def get_ai_analysis(weather_insights: str, city_name: str, temp_symbol: str) ->
return ""
url = "https://api.groq.com/openai/v1/chat/completions"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
prompt = f"""
你是一个专业的天气衍生品(如 Polymarket)交易员。你的任务是分析当前天气特征,判断今日实测最高温是否能达到或超过预报中的【最高值】。
@@ -69,18 +67,21 @@ P4 **预报背景**(最低优先级):
payload = {
"model": model,
"messages": [
{"role": "system", "content": "你是不讲废话、只看数据的专业气象分析师。"},
{"role": "user", "content": prompt}
{
"role": "system",
"content": "你是不讲废话、只看数据的专业气象分析师。",
},
{"role": "user", "content": prompt},
],
"temperature": 0.5,
"max_tokens": 250
"max_tokens": 250,
}
response = requests.post(url, json=payload, headers=headers, timeout=15)
response.raise_for_status()
result = response.json()
content = result['choices'][0]['message']['content'].strip()
content = result["choices"][0]["message"]["content"].strip()
if model != MODELS[0]:
logger.info(f"Groq 降级到备用模型 {model} 成功")
@@ -93,7 +94,9 @@ P4 **预报背景**(最低优先级):
time.sleep(1.5)
continue
else:
logger.warning(f"Groq {model} 失败 (HTTP {status}),尝试下一个模型...")
logger.warning(
f"Groq {model} 失败 (HTTP {status}),尝试下一个模型..."
)
break # 换下一个模型
except Exception as e:
logger.warning(f"Groq {model} 异常: {e},尝试下一个模型...")
@@ -101,4 +104,3 @@ P4 **预报背景**(最低优先级):
logger.error("所有 Groq 模型均不可用")
return "\n⚠️ Groq AI 暂时不可用,请稍后再试"
+44 -23
View File
@@ -1,6 +1,5 @@
import os
import json
import logging
from datetime import datetime, timedelta
import fcntl
@@ -9,6 +8,7 @@ import fcntl
_history_cache = {}
_history_mtime = 0
def load_history(filepath):
global _history_cache, _history_mtime
if not os.path.exists(filepath):
@@ -19,7 +19,7 @@ def load_history(filepath):
if current_mtime == _history_mtime and _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,
# but using one prevents reading half-written JSONs.
fcntl.flock(f, fcntl.LOCK_SH)
@@ -33,11 +33,12 @@ def load_history(filepath):
print(f"Error loading history: {e}")
return _history_cache if _history_cache else {}
def save_history(filepath, data):
global _history_cache, _history_mtime
_history_cache = data
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)
json.dump(data, f, ensure_ascii=False, indent=2)
fcntl.flock(f, fcntl.LOCK_UN)
@@ -45,15 +46,20 @@ def save_history(filepath, data):
except Exception as 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, ...}
actual_high: float, 最终实测最高温
deb_prediction: float, DEB 融合预测值用于准确率追踪
"""
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
history_file = os.path.join(project_root, 'data', 'daily_records.json')
project_root = os.path.dirname(
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)
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] = {}
# 避免无意义的频繁磁盘写入
old_actual = data[city_name][date_str].get('actual_high')
if old_actual == actual_high and data[city_name][date_str].get('forecasts') == forecasts:
old_actual = data[city_name][date_str].get("actual_high")
if (
old_actual == actual_high
and data[city_name][date_str].get("forecasts") == forecasts
):
return
data[city_name][date_str]['forecasts'] = forecasts
data[city_name][date_str]['actual_high'] = actual_high
data[city_name][date_str]["forecasts"] = forecasts
data[city_name][date_str]["actual_high"] = actual_high
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 天留足余量)
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)
def calculate_dynamic_weights(city_name, current_forecasts, lookback_days=7):
"""
计算动态权重融合 (Dynamic Ensemble Blending, DEB)
根据过去 N 天各模型的 Mean Absolute Error (MAE) 计算倒数权重
返回: blended_high (融合预报值), weights_info (权重展示字符串)
"""
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
history_file = os.path.join(project_root, 'data', 'daily_records.json')
project_root = os.path.dirname(
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)
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]
if not valid_vals: return None, "暂无模型数据"
if not valid_vals:
return None, "暂无模型数据"
avg = sum(valid_vals) / len(valid_vals)
return round(avg, 1), "等权平均(历史数据不足)"
@@ -113,8 +126,8 @@ def calculate_dynamic_weights(city_name, current_forecasts, lookback_days=7):
continue
record = city_data[date_str]
actual = record.get('actual_high')
past_forecasts = record.get('forecasts', {})
actual = record.get("actual_high")
past_forecasts = record.get("forecasts", {})
if actual is None:
continue
@@ -143,7 +156,11 @@ def calculate_dynamic_weights(city_name, current_forecasts, lookback_days=7):
maes[model] = 2.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())
if total_inv == 0:
@@ -174,8 +191,10 @@ def get_deb_accuracy(city_name):
- total_days: 有效天数
- details_str: 格式化的展示字符串
"""
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
history_file = os.path.join(project_root, 'data', 'daily_records.json')
project_root = os.path.dirname(
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)
if city_name not in data:
@@ -192,8 +211,8 @@ def get_deb_accuracy(city_name):
if date_str == today_str:
continue # 跳过今天,还没结算
record = city_data[date_str]
deb_pred = record.get('deb_prediction')
actual = record.get('actual_high')
deb_pred = record.get("deb_prediction")
actual = record.get("actual_high")
if deb_pred is None or actual is None:
continue
@@ -201,7 +220,7 @@ def get_deb_accuracy(city_name):
try:
deb_pred = float(deb_pred)
actual = float(actual)
except:
except Exception:
continue
total += 1
@@ -217,6 +236,8 @@ def get_deb_accuracy(city_name):
hit_rate = hits / total * 100
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
+1 -7
View File
@@ -27,7 +27,6 @@ CITY_RISK_PROFILES = {
"warning": "冬天温差最不稳定",
"season_notes": "冬季",
},
# 🟡 中危城市 - 存在系统偏差,需注意
"ankara": {
"risk_level": "medium",
@@ -90,7 +89,6 @@ CITY_RISK_PROFILES = {
"warning": "机场在北郊,冬季北风时比市区更冷",
"season_notes": "夏季热浪期间偏差最大",
},
# 🟢 低危城市 - 数据相对靠谱
"toronto": {
"risk_level": "low",
@@ -183,11 +181,7 @@ def format_risk_warning(profile: dict, temp_symbol: str) -> str:
lines = []
# 风险等级标题
risk_labels = {
"high": "高危",
"medium": "中危",
"low": "低危"
}
risk_labels = {"high": "高危", "medium": "中危", "low": "低危"}
risk_label = risk_labels.get(profile["risk_level"], "未知")
lines.append(f"⚠️ <b>数据偏差风险</b>: {profile['risk_emoji']} {risk_label}")
+189 -68
View File
@@ -207,7 +207,9 @@ class WeatherDataCollector:
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 航空气象数据
@@ -239,7 +241,7 @@ class WeatherDataCollector:
url,
params=params,
headers={"Cache-Control": "no-cache", "Pragma": "no-cache"},
timeout=self.timeout
timeout=self.timeout,
)
response.raise_for_status()
@@ -258,39 +260,53 @@ class WeatherDataCollector:
"""从 rawOb 中提取精确的 UTC 观测时间"""
raw = obs.get("rawOb", "")
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:
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 的时分
fallback = obs.get("reportTime", "")
try:
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"))
result = base_dt.replace(hour=hour, minute=minute, second=0)
# 处理跨日(如 rawOb 是23:50但 reportTime 已经是次日00:00
if result > base_dt + timedelta(hours=2):
result -= timedelta(days=1)
return result
except:
except Exception:
pass
# fallback 到 reportTime
fallback = obs.get("reportTime", "")
try:
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"))
except:
except Exception:
return None
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. 精确计算"当地今天"的最高温
from datetime import timezone, timedelta
now_utc = datetime.now(timezone.utc)
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)
max_so_far_c = -999
@@ -306,7 +322,7 @@ class WeatherDataCollector:
max_so_far_c = t
local_report = obs_dt_iter + timedelta(seconds=utc_offset)
max_temp_time = local_report.strftime("%H:%M")
except:
except Exception:
continue
# 3. 提取最近 4 条报文的多维数据(温度 + 风/云/压强,用于趋势和 shock_score
@@ -319,21 +335,30 @@ class WeatherDataCollector:
local_rt = obs_dt_iter + timedelta(seconds=utc_offset)
recent_temps_raw.append((local_rt.strftime("%H:%M"), obs_temp))
# 云量码映射: 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", [])
max_cloud_rank = 0
for c in clouds:
rank = cloud_rank_map.get(c.get("cover", ""), 0)
if rank > max_cloud_rank:
max_cloud_rank = rank
recent_obs_raw.append({
recent_obs_raw.append(
{
"time": local_rt.strftime("%H:%M"),
"temp": obs_temp,
"wdir": obs.get("wdir"),
"wspd": obs.get("wspd"),
"cloud_rank": max_cloud_rank, # 0~4
"altim": obs.get("altim"),
})
}
)
# 转换为单位
if use_fahrenheit:
@@ -342,7 +367,9 @@ class WeatherDataCollector:
dewp = dewp_c * 9 / 5 + 32 if dewp_c is not None else None
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:
temp = temp_c
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,
"current": {
"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,
"dewpoint": round(dewp, 1) if dewp is not None else None,
"humidity": latest.get("rh"),
@@ -396,17 +425,18 @@ class WeatherDataCollector:
# 必须带 Origin,否则会被反爬拦截
headers = {
"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 = {}
try:
# 1. 实时数据 (添加时间戳防止 CDN 缓存)
import time
obs_resp = self.session.get(
f"{base_url}/sondurumlar?istno={istno}&_={int(time.time() * 1000)}",
headers=headers,
timeout=self.timeout
timeout=self.timeout,
)
if obs_resp.status_code == 200:
data = obs_resp.json()
@@ -421,18 +451,39 @@ class WeatherDataCollector:
return v is not None and v > -9000
results["current"] = {
"temp": latest.get("sicaklik") if _valid(latest.get("sicaklik")) else None,
"feels_like": latest.get("hissedilenSicaklik") if _valid(latest.get("hissedilenSicaklik")) else None,
"humidity": latest.get("nem") 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,
"temp": latest.get("sicaklik")
if _valid(latest.get("sicaklik"))
else None,
"feels_like": latest.get("hissedilenSicaklik")
if _valid(latest.get("hissedilenSicaklik"))
else None,
"humidity": latest.get("nem")
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 八分位云量
"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"),
"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 路径)
@@ -442,7 +493,9 @@ class WeatherDataCollector:
]
for forecast_url in forecast_urls:
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:
forecasts = daily_resp.json()
if forecasts and isinstance(forecasts, list):
@@ -452,14 +505,26 @@ class WeatherDataCollector:
if high_val is not None:
results["today_high"] = high_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
else:
# 记录所有可用字段,方便调试
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}")
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:
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:
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}"
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_data = points_resp.json()
@@ -487,7 +554,9 @@ class WeatherDataCollector:
return None
# 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_data = forecast_resp.json()
@@ -574,7 +643,7 @@ class WeatherDataCollector:
# 记录今日模型分歧
daily_data["model_split"] = {
"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 补全
@@ -596,7 +665,9 @@ class WeatherDataCollector:
# 映射逐小时数据
hourly_data = data.get("hourly", {})
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()
@@ -778,7 +849,9 @@ class WeatherDataCollector:
forecasts = daily_forecasts.get(today_date, {})
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 {
"source": "multi_model",
@@ -814,14 +887,10 @@ class WeatherDataCollector:
"lat": lat,
"lon": lon,
"format": "json",
"as_daylight": "true"
"as_daylight": "true",
}
response = self.session.get(
url,
params=params,
timeout=self.timeout
)
response = self.session.get(url, params=params, timeout=self.timeout)
response.raise_for_status()
data = response.json()
@@ -829,7 +898,9 @@ class WeatherDataCollector:
max_temps = day_data.get("temperature_max", [])
if not max_temps:
logger.warning(f"Meteoblue API 返回数据中找不到最高温 (坐标: {lat},{lon})")
logger.warning(
f"Meteoblue API 返回数据中找不到最高温 (坐标: {lat},{lon})"
)
return None
# 2. 转换单位
@@ -841,7 +912,7 @@ class WeatherDataCollector:
"today_high": None,
"daily_highs": [],
"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:
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
except Exception as e:
logger.error(f"Meteoblue API fetch failed: {e}")
@@ -867,9 +940,18 @@ class WeatherDataCollector:
"""
# 1. 尝试英文月份
months = {
"January": "01", "February": "02", "March": "03", "April": "04",
"May": "05", "June": "06", "July": "07", "August": "08",
"September": "09", "October": "10", "November": "11", "December": "12",
"January": "01",
"February": "02",
"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():
if month_name in title:
@@ -956,18 +1038,32 @@ class WeatherDataCollector:
# 1. 优先尝试已知城市列表 (硬编码匹配)
known_cities = {
"london": "London", "伦敦": "London",
"new york": "New York", "new york's central park": "New York", "nyc": "New York", "纽约": "New York",
"seattle": "Seattle", "西雅图": "Seattle",
"chicago": "Chicago", "芝加哥": "Chicago",
"dallas": "Dallas", "达拉斯": "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"
"london": "London",
"伦敦": "London",
"new york": "New York",
"new york's central park": "New York",
"nyc": "New York",
"纽约": "New York",
"seattle": "Seattle",
"西雅图": "Seattle",
"chicago": "Chicago",
"芝加哥": "Chicago",
"dallas": "Dallas",
"达拉斯": "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():
@@ -975,11 +1071,31 @@ class WeatherDataCollector:
return val
# 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:
if trigger in q:
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
for d in delimiters:
if d in city:
@@ -1039,7 +1155,9 @@ class WeatherDataCollector:
results["open-meteo"] = open_meteo
# 获取时区偏移以过滤 METAR
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:
results["metar"] = metar_data
@@ -1052,9 +1170,10 @@ class WeatherDataCollector:
# 对伦敦,获取 Meteoblue 预测 (公认最准)
if city_lower == "london":
mb_data = self.fetch_from_meteoblue(
lat, lon,
lat,
lon,
timezone_name=open_meteo.get("timezone", "UTC"),
use_fahrenheit=use_fahrenheit
use_fahrenheit=use_fahrenheit,
)
if mb_data:
results["meteoblue"] = mb_data
@@ -1071,7 +1190,9 @@ class WeatherDataCollector:
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:
results["multi_model"] = mm_data
else:
+27 -15
View File
@@ -1,24 +1,25 @@
import os
import sys
import json
import logging
from datetime import datetime
import pandas as pd
import requests
# 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):
city_name = city_info['city']
lat = city_info['latitude']
lon = city_info['longitude']
city_name = city_info["city"]
lat = city_info["latitude"]
lon = city_info["longitude"]
# We will fetch data from Jan 1, 2023 to yesterday (or to latest available)
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
import datetime
today = datetime.datetime.now()
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"
)
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:
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)
# 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)
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:
logging.error(f"❌ Network error while fetching data for {city_name}: {e}")
except Exception as e:
logging.error(f"❌ Error processing data for {city_name}: {e}")
def main():
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
config_path = os.path.join(project_root, 'config', 'config.yaml')
output_dir = os.path.join(project_root, 'data', 'historical')
project_root = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
)
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)
# Load config
try:
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)
except Exception as e:
logging.error(f"Failed to load {config_path}: {e}")
sys.exit(1)
cities = config.get('cities', [])
cities = config.get("cities", [])
if not cities:
logging.warning("No cities found in config.yaml")
return
@@ -81,5 +92,6 @@ def main():
for city_info in cities:
fetch_historical_data_for_city(city_info, output_dir)
if __name__ == "__main__":
main()
+3 -2
View File
@@ -1,6 +1,7 @@
import os
from dotenv import load_dotenv
def load_config():
"""
Load configuration from environment variables and config files
@@ -40,14 +41,14 @@ def load_config():
"market_volume_signal": 0.15,
"orderbook_analysis": 0.10,
"technical_indicators": 0.05,
"onchain_whale_signal": 0.05
"onchain_whale_signal": 0.05,
}
},
"app": {
"log_level": os.getenv("LOG_LEVEL", "INFO"),
"env": os.getenv("ENV", "development"),
"proxy": os.getenv("HTTPS_PROXY") or os.getenv("HTTP_PROXY"),
}
},
}
return config
+3 -2
View File
@@ -1,6 +1,7 @@
import sys
from loguru import logger
def setup_logger(level="DEBUG"):
"""
Configure loguru logger
@@ -11,7 +12,7 @@ def setup_logger(level="DEBUG"):
logger.add(
sys.stderr,
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",
level=level,
encoding="utf-8",
compression="zip"
compression="zip",
)
logger.info("日志系统初始化完成。")