feat: Introduce comprehensive weather data querying, analysis, and display services, integrate Polymarket data collection, and add Telegram notification utilities.

This commit is contained in:
2569718930@qq.com
2026-03-11 10:23:33 +08:00
parent 878e3280d1
commit b3f46430ad
9 changed files with 505 additions and 63 deletions
+62 -8
View File
@@ -58,6 +58,17 @@ def _render_local_time(
metar: Dict[str, Any],
fallback_utc_offset: int,
) -> str:
utc_offset = open_meteo.get("utc_offset")
if utc_offset is None:
utc_offset = fallback_utc_offset
try:
local_now = datetime.now(timezone.utc).astimezone(
timezone(timedelta(seconds=int(utc_offset)))
)
return local_now.strftime("%H:%M")
except Exception:
pass
local_time = (open_meteo.get("current") or {}).get("local_time", "")
if " " in str(local_time):
return str(local_time).split(" ")[1][:5]
@@ -79,13 +90,49 @@ def _render_local_time(
if metar_obs:
return str(metar_obs)[:5]
try:
local_now = datetime.now(timezone.utc).astimezone(
timezone(timedelta(seconds=int(fallback_utc_offset)))
)
return local_now.strftime("%H:%M")
except Exception:
return "N/A"
return "N/A"
def _derive_mgm_daily_highs_from_hourly(
mgm: Dict[str, Any],
fallback_utc_offset: int,
) -> Dict[str, float]:
if not isinstance(mgm, dict):
return {}
hourly = mgm.get("hourly")
if not isinstance(hourly, list) or not hourly:
return {}
daily_highs: Dict[str, float] = {}
local_tz = timezone(timedelta(seconds=int(fallback_utc_offset)))
for row in hourly:
if not isinstance(row, dict):
continue
temp = _sf(row.get("temp"))
raw_time = str(row.get("time") or "").strip()
if temp is None or not raw_time:
continue
date_key = None
if "T" in raw_time:
try:
dt = datetime.fromisoformat(raw_time.replace("Z", "+00:00"))
if dt.tzinfo is not None:
dt = dt.astimezone(local_tz)
date_key = dt.strftime("%Y-%m-%d")
except Exception:
if len(raw_time) >= 10 and raw_time[4] == "-" and raw_time[7] == "-":
date_key = raw_time[:10]
elif len(raw_time) >= 10 and raw_time[4] == "-" and raw_time[7] == "-":
date_key = raw_time[:10]
if not date_key:
continue
prev = daily_highs.get(date_key)
daily_highs[date_key] = temp if prev is None else max(prev, temp)
return daily_highs
def _append_future_forecast_lines(
@@ -98,6 +145,12 @@ def _append_future_forecast_lines(
) -> None:
mgm = weather_data.get("mgm") or {}
mgm_daily = (mgm.get("daily_forecasts") or {}) if isinstance(mgm, dict) else {}
mgm_hourly_daily = _derive_mgm_daily_highs_from_hourly(mgm, fallback_utc_offset)
if not isinstance(mgm_daily, dict):
mgm_daily = {}
for date_key, day_high in mgm_hourly_daily.items():
if date_key not in mgm_daily:
mgm_daily[date_key] = day_high
mm_raw = weather_data.get("multi_model") or {}
mm_daily = mm_raw.get("daily_forecasts", {}) if isinstance(mm_raw, dict) else {}
mb_daily = (weather_data.get("meteoblue") or {}).get("daily_highs", []) or []
@@ -108,8 +161,9 @@ def _append_future_forecast_lines(
for d, t in zip(dates[1:], max_temps[1:]):
mgm_value = mgm_daily.get(d) if isinstance(mgm_daily, dict) else None
if mgm_value is not None:
mgm_display = f"{float(mgm_value):.1f}"
future_forecasts.append(
f"{d[5:]}: {t}{temp_symbol} | 🇺🇸 <b>MGM: {mgm_value}{temp_symbol}</b>"
f"{d[5:]}: {t}{temp_symbol} | 🇺🇸 <b>MGM: {mgm_display}{temp_symbol}</b>"
)
else:
future_forecasts.append(f"{d[5:]}: {t}{temp_symbol}")
+36 -3
View File
@@ -473,6 +473,18 @@ def _bucket_label(bucket: Any) -> Optional[str]:
return None
def _row_yes_buy_prob(row: Dict[str, Any]) -> Optional[float]:
if not isinstance(row, dict):
return None
return _norm_probability(row.get("yes_buy"))
def _has_actionable_yes_buy_quote(row: Dict[str, Any]) -> bool:
quote = _row_yes_buy_prob(row)
# 0 usually means no actionable orderbook bid, not a tradable quote.
return quote is not None and quote > 0.0
def _to_celsius(temp: Optional[float], temp_symbol: str) -> Optional[float]:
if temp is None:
return None
@@ -555,6 +567,7 @@ def _pick_bucket_for_forecast(
best_row: Optional[Dict[str, Any]] = None
best_distance: Optional[float] = None
best_has_quote = False
best_probability = -1.0
best_rank = 10**9
@@ -564,12 +577,14 @@ def _pick_bucket_for_forecast(
continue
distance = _distance_to_bucket(target, bounds)
has_quote = _has_actionable_yes_buy_quote(row)
probability = _norm_probability(row.get("probability"))
probability_rank = probability if probability is not None else -1.0
if best_row is None:
best_row = row
best_distance = distance
best_has_quote = has_quote
best_probability = probability_rank
best_rank = idx
continue
@@ -578,19 +593,32 @@ def _pick_bucket_for_forecast(
if distance < best_distance:
best_row = row
best_distance = distance
best_has_quote = has_quote
best_probability = probability_rank
best_rank = idx
continue
if abs(distance - best_distance) <= 1e-9:
if probability_rank > best_probability:
if has_quote and not best_has_quote:
best_row = row
best_distance = distance
best_has_quote = has_quote
best_probability = probability_rank
best_rank = idx
elif abs(probability_rank - best_probability) <= 1e-9 and idx < best_rank:
elif has_quote == best_has_quote and probability_rank > best_probability:
best_row = row
best_distance = distance
best_has_quote = has_quote
best_probability = probability_rank
best_rank = idx
elif (
has_quote == best_has_quote
and abs(probability_rank - best_probability) <= 1e-9
and idx < best_rank
):
best_row = row
best_distance = distance
best_has_quote = has_quote
best_probability = probability_rank
best_rank = idx
@@ -972,7 +1000,12 @@ def _build_telegram_messages_mispricing(
om_settle = snapshot.get("open_meteo_settlement")
forecast_bucket = snapshot.get("forecast_bucket") or {}
match_bucket_label = str(forecast_bucket.get("label") or "--").strip() or "--"
match_bucket_yes = _fmt_cents(forecast_bucket.get("yes_buy"))
match_bucket_yes_prob = _norm_probability(forecast_bucket.get("yes_buy"))
match_bucket_yes = (
_fmt_cents(match_bucket_yes_prob)
if match_bucket_yes_prob is not None and match_bucket_yes_prob > 0.0
else "--"
)
market_url = str(
snapshot.get("market_url")
or snapshot.get("primary_market_url")