diff --git a/src/analysis/city_query_service.py b/src/analysis/city_query_service.py
index 751ac789..1e74df69 100644
--- a/src/analysis/city_query_service.py
+++ b/src/analysis/city_query_service.py
@@ -103,7 +103,8 @@ def _derive_mgm_daily_highs_from_hourly(
if not isinstance(hourly, list) or not hourly:
return {}
- daily_highs: Dict[str, float] = {}
+ samples: List[Tuple[str, float]] = []
+ parsed_datetimes: List[datetime] = []
local_tz = timezone(timedelta(seconds=int(fallback_utc_offset)))
for row in hourly:
if not isinstance(row, dict):
@@ -119,6 +120,9 @@ def _derive_mgm_daily_highs_from_hourly(
dt = datetime.fromisoformat(raw_time.replace("Z", "+00:00"))
if dt.tzinfo is not None:
dt = dt.astimezone(local_tz)
+ else:
+ dt = dt.replace(tzinfo=local_tz)
+ parsed_datetimes.append(dt)
date_key = dt.strftime("%Y-%m-%d")
except Exception:
if len(raw_time) >= 10 and raw_time[4] == "-" and raw_time[7] == "-":
@@ -129,6 +133,24 @@ def _derive_mgm_daily_highs_from_hourly(
if not date_key:
continue
+ samples.append((date_key, temp))
+
+ if not samples:
+ return {}
+
+ # Guardrail: do not derive "daily highs" from short intraday snippets.
+ if parsed_datetimes:
+ parsed_datetimes.sort()
+ horizon_hours = (
+ parsed_datetimes[-1] - parsed_datetimes[0]
+ ).total_seconds() / 3600.0
+ if horizon_hours < 30:
+ return {}
+ elif len(samples) < 24:
+ return {}
+
+ daily_highs: Dict[str, float] = {}
+ for date_key, temp in samples:
prev = daily_highs.get(date_key)
daily_highs[date_key] = temp if prev is None else max(prev, temp)
@@ -163,7 +185,7 @@ def _append_future_forecast_lines(
if mgm_value is not None:
mgm_display = f"{float(mgm_value):.1f}"
future_forecasts.append(
- f"{d[5:]}: {t}{temp_symbol} | πΊπΈ MGM: {mgm_display}{temp_symbol}"
+ f"{d[5:]}: {t}{temp_symbol} | MGM: {mgm_display}{temp_symbol}"
)
else:
future_forecasts.append(f"{d[5:]}: {t}{temp_symbol}")
diff --git a/src/data_collection/weather_sources.py b/src/data_collection/weather_sources.py
index 179a291d..0ffd71db 100644
--- a/src/data_collection/weather_sources.py
+++ b/src/data_collection/weather_sources.py
@@ -768,22 +768,48 @@ class WeatherDataCollector:
# 5. Fallback for daily_forecasts from hourly data
if not results.get("daily_forecasts") and results.get("hourly"):
- from collections import defaultdict
- daily_max = defaultdict(list)
- for h in results["hourly"]:
- t = h.get("time", "")
- temp = h.get("temp")
- if t and temp is not None:
- # Extract date from ISO timestamp like "2026-03-05T12:00:00.000Z"
- date_str = t[:10]
- daily_max[date_str].append(temp)
- if daily_max:
- results["daily_forecasts"] = {}
- for d, temps in sorted(daily_max.items()):
- results["daily_forecasts"][d] = max(temps)
+ # Guardrail: avoid treating short intraday snippets as full-day highs.
+ hourly_rows = results.get("hourly") or []
+ parsed_times = []
+ for h in hourly_rows:
+ t = str(h.get("time") or "")
+ if "T" not in t:
+ continue
+ try:
+ parsed_times.append(datetime.fromisoformat(t.replace("Z", "+00:00")))
+ except Exception:
+ continue
+
+ horizon_hours = 0.0
+ if len(parsed_times) >= 2:
+ parsed_times.sort()
+ horizon_hours = (
+ parsed_times[-1] - parsed_times[0]
+ ).total_seconds() / 3600.0
+
+ if len(hourly_rows) >= 24 or horizon_hours >= 30:
+ from collections import defaultdict
+
+ daily_max = defaultdict(list)
+ for h in hourly_rows:
+ t = h.get("time", "")
+ temp = h.get("temp")
+ if t and temp is not None:
+ # Extract date from ISO timestamp like "2026-03-05T12:00:00.000Z"
+ date_str = t[:10]
+ daily_max[date_str].append(temp)
+ if daily_max:
+ results["daily_forecasts"] = {}
+ for d, temps in sorted(daily_max.items()):
+ results["daily_forecasts"][d] = max(temps)
+ logger.info(
+ f"π MGM daily_forecasts (from hourly fallback): "
+ f"{dict(results['daily_forecasts'])}"
+ )
+ else:
logger.info(
- f"π MGM daily_forecasts (from hourly fallback): "
- f"{dict(results['daily_forecasts'])}"
+ "π Skip MGM daily_forecasts hourly fallback: "
+ f"hourly points={len(hourly_rows)}, horizon={horizon_hours:.1f}h"
)
return results if "current" in results else None