feat: Introduce comprehensive weather data querying, analysis, and display services, integrate Polymarket data collection, and add Telegram notification utilities.
This commit is contained in:
@@ -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}")
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user