diff --git a/config/config.yaml b/config/config.yaml index 68ffe921..09ef9015 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -44,8 +44,8 @@ cities: - id: hong_kong city: Hong Kong country: China - latitude: 22.308 - longitude: 113.9185 + latitude: 22.3019 + longitude: 114.1742 - id: shanghai city: Shanghai country: China diff --git a/src/analysis/city_query_service.py b/src/analysis/city_query_service.py index 20f630c2..989ceba3 100644 --- a/src/analysis/city_query_service.py +++ b/src/analysis/city_query_service.py @@ -7,6 +7,7 @@ from src.analysis.metar_narrator import describe_metar_report from src.analysis.trend_engine import analyze_weather_trend from src.data_collection.city_registry import ALIASES, CITY_REGISTRY from src.data_collection.city_risk_profiles import get_city_risk_profile +from src.analysis.settlement_rounding import apply_city_settlement FAHRENHEIT_CITIES = { @@ -505,7 +506,7 @@ def build_city_query_report( max_str = "" if max_p is not None: - settled_val = int(max_p + 0.5) + settled_val = apply_city_settlement(city_name.lower(), max_p) max_str = f" (最高: {max_p}{temp_symbol}" if max_p_time: max_str += f" @{max_p_time}" @@ -520,12 +521,19 @@ def build_city_query_report( ) if use_settlement_current: + # HKO/CWA detailed observations wind = primary_current.get("wind_speed_kt") wind_dir = primary_current.get("wind_dir") humidity = primary_current.get("humidity") msg_lines.append( f" [{settlement_source_label}] 🌪 {wind or 0}kt ({wind_dir or 0}°) | 💧 湿度: {humidity or 'N/A'}%" ) + # Secondary METAR info if available for context + if metar: + m_wind = metar_current.get("wind_speed_kt") + m_dir = metar_current.get("wind_dir") + m_vis = metar_current.get("visibility_mi") + msg_lines.append(f" [METAR] 🌪 {m_wind or 0}kt ({m_dir or 0}°) | 👁️ {m_vis or 10}mi") elif metar: wind = metar_current.get("wind_speed_kt") wind_dir = metar_current.get("wind_dir") diff --git a/src/analysis/trend_engine.py b/src/analysis/trend_engine.py index 17d7a9fb..424ebe90 100644 --- a/src/analysis/trend_engine.py +++ b/src/analysis/trend_engine.py @@ -127,6 +127,11 @@ def analyze_weather_trend( mgm = weather_data.get("mgm", {}) if mgm and mgm.get("today_high") is not None: current_forecasts["MGM"] = _sf(mgm.get("today_high")) + + if weather_data.get("hko_forecast") is not None: + current_forecasts["HKO(港天文)"] = _sf(weather_data.get("hko_forecast")) + if weather_data.get("cwa_forecast") is not None: + current_forecasts["CWA(台气象)"] = _sf(weather_data.get("cwa_forecast")) mm_forecasts = weather_data.get("multi_model", {}).get("forecasts", {}) for m_name, m_val in mm_forecasts.items(): @@ -515,23 +520,40 @@ def analyze_weather_trend( # === Settlement boundary === if max_so_far is not None: settled = apply_city_settlement(city_name, max_so_far) + from src.analysis.settlement_rounding import is_exact_settlement_city + is_floor = is_exact_settlement_city(str(city_name).lower()) + fractional = max_so_far - int(max_so_far) - dist_to_boundary = abs(fractional - 0.5) - if dist_to_boundary <= 0.3: - if fractional < 0.5: + + if is_floor: + # For flooring cities like HK, boundary is at 1.0 (approaching next integer) + dist_to_next = 1.0 - fractional + if dist_to_next <= 0.3: msg = ( f"⚖️ 结算边界:当前最高 {max_so_far}{temp_symbol} → {settlement_source_label} 结算 " - f"{settled}{temp_symbol},但只差 {0.5 - fractional:.1f}° " + f"{settled}{temp_symbol},但只差 {dist_to_next:.1f}° " f"就会进位到 {settled + 1}{temp_symbol}!" ) - else: - msg = ( - f"⚖️ 结算边界:当前最高 {max_so_far}{temp_symbol} → {settlement_source_label} 结算 " - f"{settled}{temp_symbol},刚刚越过进位线,再降 " - f"{fractional - 0.5:.1f}° 就会回落到 {settled - 1}{temp_symbol}。" - ) - insights.append(msg) - ai_features.append(msg) + insights.append(msg) + ai_features.append(msg) + else: + # Standard rounding boundary at 0.5 + dist_to_boundary = abs(fractional - 0.5) + if dist_to_boundary <= 0.3: + if fractional < 0.5: + msg = ( + f"⚖️ 结算边界:当前最高 {max_so_far}{temp_symbol} → {settlement_source_label} 结算 " + f"{settled}{temp_symbol},但只差 {0.5 - fractional:.1f}° " + f"就会进位到 {settled + 1}{temp_symbol}!" + ) + else: + msg = ( + f"⚖️ 结算边界:当前最高 {max_so_far}{temp_symbol} → {settlement_source_label} 结算 " + f"{settled}{temp_symbol},刚刚越过进位线,再降 " + f"{fractional - 0.5:.1f}° 就会回落到 {settled - 1}{temp_symbol}。" + ) + insights.append(msg) + ai_features.append(msg) # === Peak window AI hints === if peak_hours: diff --git a/src/data_collection/weather_sources.py b/src/data_collection/weather_sources.py index a5bf3c55..bea2442b 100644 --- a/src/data_collection/weather_sources.py +++ b/src/data_collection/weather_sources.py @@ -510,6 +510,32 @@ class WeatherDataCollector: logger.warning(f"CWA settlement fetch failed: {exc}") return None + def fetch_hko_forecast(self) -> Optional[float]: + try: + url = "https://data.weather.gov.hk/weatherAPI/opendata/weather.php?dataType=fnd&lang=tc" + res = self.session.get(url, timeout=self.timeout).json() + return float(res['weatherForecast'][0]['forecastMaxtemp']['value']) + except Exception as e: + logger.warning(f"HKO Forecast request failed: {e}") + return None + + def fetch_cwa_taipei_forecast(self) -> Optional[float]: + try: + if not self.cwa_open_data_auth: + return None + url = 'https://opendata.cwa.gov.tw/api/v1/rest/datastore/F-D0047-061' + res = self.session.get(url, params={'Authorization': self.cwa_open_data_auth, 'format': 'JSON', 'elementName': 'MaxT'}, timeout=self.timeout).json() + locs = res.get('records', {}).get('Locations', [])[0].get('Location', []) + if not locs: return None + loc = locs[0] + for we in loc.get('WeatherElement', []): + if we.get('ElementName') == 'MaxT': + return float(we['Time'][0]['ElementValue'][0]['Temperature']) + return None + except Exception as e: + logger.warning(f"CWA Forecast request failed: {e}") + return None + def fetch_settlement_current(self, city: str) -> Optional[Dict[str, Any]]: normalized = str(city or "").strip().lower() if normalized == "hong kong": @@ -2146,6 +2172,15 @@ class WeatherDataCollector: if settlement_current: results["settlement_current"] = settlement_current + if city_lower in ["hong_kong", "hong kong", "香港", "hk"]: + hko_fcst = self.fetch_hko_forecast() + if hko_fcst: + results["hko_forecast"] = hko_fcst + elif city_lower in ["taipei", "台北", "臺北", "tpe"]: + cwa_fcst = self.fetch_cwa_taipei_forecast() + if cwa_fcst: + results["cwa_forecast"] = cwa_fcst + if lat and lon: open_meteo = self.fetch_from_open_meteo( lat, lon, use_fahrenheit=use_fahrenheit diff --git a/web/app.py b/web/app.py index 67cc2d57..26992a8c 100644 --- a/web/app.py +++ b/web/app.py @@ -31,7 +31,7 @@ from src.data_collection.weather_sources import WeatherDataCollector from src.data_collection.city_risk_profiles import CITY_RISK_PROFILES from src.data_collection.polymarket_readonly import PolymarketReadOnlyLayer from src.analysis.deb_algorithm import calculate_dynamic_weights, get_deb_accuracy -from src.analysis.settlement_rounding import wu_round +from src.analysis.settlement_rounding import wu_round, apply_city_settlement from src.auth.supabase_entitlement import ( SUPABASE_ENTITLEMENT, extract_bearer_token, @@ -394,7 +394,7 @@ def _analyze(city: str, force_refresh: bool = False) -> Dict[str, Any]: if max_temp_time == "": max_temp_time = None - wu_settle = wu_round(max_so_far) if max_so_far is not None else None + wu_settle = apply_city_settlement(city.lower(), max_so_far) if max_so_far is not None else None # Observation time → local obs_time_str = "" @@ -1075,7 +1075,7 @@ def _build_city_detail_payload( temp_symbol = str(data.get("temp_symbol") or "") if anchor_temp_c is not None and "F" in temp_symbol.upper(): anchor_temp_c = (anchor_temp_c - 32.0) * 5.0 / 9.0 - anchor_settlement = wu_round(anchor_temp_c) if anchor_temp_c is not None else None + anchor_settlement = apply_city_settlement(city.lower(), anchor_temp_c) if anchor_temp_c is not None else None primary_bucket = None if isinstance(distribution, list) and distribution: