"""Open-Meteo client for WeatherNext and other weather model data.""" from datetime import datetime, timedelta from typing import Optional import numpy as np import pandas as pd try: import openmeteo_requests import requests_cache from retry_requests import retry except ImportError: import subprocess, sys subprocess.check_call([sys.executable, "-m", "pip", "install", "openmeteo-requests", "requests-cache", "retry-requests"]) import openmeteo_requests import requests_cache from retry_requests import retry from config import HK_COORDS, HK_BBOX, OPENMETEO_API_KEY class OpenMeteoClient: """Fetch weather data from Open-Meteo including WeatherNext model outputs.""" BASE_URL = "https://api.open-meteo.com/v1/" # Available weather models via Open-Meteo MODELS = { "weathernext": "google_weathernext", "ecmwf": "ecmwf_ifs04", "gfs": "gfs_seamless", } def __init__(self, model: str = "weathernext", cache_ttl: int = 3600): cache = requests_cache.CachedSession('.openmeteo_cache', expire_after=cache_ttl) retry_session = retry(cache, retries=3, backoff_factor=0.2) self.client = openmeteo_requests.Client(session=retry_session) self.model = model self.params = { "latitude": HK_COORDS["hko_headquarters"][0], "longitude": HK_COORDS["hko_headquarters"][1], "timezone": "Asia/Hong_Kong", } def get_forecast(self, lead_days: int = 7) -> Optional[pd.DataFrame]: """Fetch forecast for Hong Kong from selected model.""" params = { **self.params, "daily": [ "temperature_2m_max", "temperature_2m_min", "temperature_2m_mean", "precipitation_sum", "precipitation_probability_max", "rain_sum", "wind_speed_10m_max", "wind_gusts_10m_max", "wind_direction_10m_dominant", "shortwave_radiation_sum", "et0_fao_evapotranspiration", "weather_code", ], "hourly": [ "temperature_2m", "relative_humidity_2m", "dew_point_2m", "apparent_temperature", "precipitation_probability", "precipitation", "rain", "cloud_cover", "cloud_cover_low", "cloud_cover_mid", "cloud_cover_high", "wind_speed_10m", "wind_speed_100m", "wind_gusts_10m", "wind_direction_10m", "wind_direction_100m", "surface_pressure", "visibility", ], "past_days": 0, "forecast_days": lead_days, } if OPENMETEO_API_KEY: params["apikey"] = OPENMETEO_API_KEY try: responses = self.client.weather_api(self.BASE_URL + "forecast", params=params) return self._parse_response(responses[0]) except Exception as e: print(f"Open-Meteo API error: {e}") return None def _parse_response(self, response) -> pd.DataFrame: """Parse Open-Meteo response into a DataFrame.""" hourly = response.Hourly() daily = response.Daily() self._hourly_vars = [ "temperature_2m", "relative_humidity_2m", "dew_point_2m", "apparent_temperature", "precipitation_probability", "precipitation", "rain", "cloud_cover", "cloud_cover_low", "cloud_cover_mid", "cloud_cover_high", "wind_speed_10m", "wind_speed_100m", "wind_gusts_10m", "wind_direction_10m", "wind_direction_100m", "surface_pressure", "visibility", ] self._daily_vars = [ "temperature_2m_max", "temperature_2m_min", "temperature_2m_mean", "precipitation_sum", "precipitation_probability_max", "rain_sum", "wind_speed_10m_max", "wind_gusts_10m_max", "wind_direction_10m_dominant", "shortwave_radiation_sum", "et0_fao_evapotranspiration", "weather_code", ] hourly_data = { "date": pd.date_range( start=pd.Timestamp(hourly.Time(), unit="s", tz="UTC"), end=pd.Timestamp(hourly.TimeEnd(), unit="s", tz="UTC"), freq=pd.Timedelta(seconds=hourly.Interval()), inclusive="left", ) } for i in range(hourly.VariablesLength()): var = hourly.Variables(i) if i < len(self._hourly_vars): hourly_data[self._hourly_vars[i]] = var.ValuesAsNumpy() hourly_df = pd.DataFrame(hourly_data).set_index("date") hourly_df.index = hourly_df.index.tz_convert("Asia/Hong_Kong") daily_data = { "date": pd.date_range( start=pd.Timestamp(daily.Time(), unit="s", tz="UTC"), end=pd.Timestamp(daily.TimeEnd(), unit="s", tz="UTC"), freq=pd.Timedelta(seconds=daily.Interval()), inclusive="left", ) } for i in range(daily.VariablesLength()): var = daily.Variables(i) if i < len(self._daily_vars): daily_data[self._daily_vars[i]] = var.ValuesAsNumpy() daily_df = pd.DataFrame(daily_data).set_index("date") daily_df.index = daily_df.index.tz_convert("Asia/Hong_Kong") daily_df.attrs["model"] = self.model daily_df.attrs["hourly"] = hourly_df daily_df.attrs["fetch_time"] = datetime.now() return daily_df def get_current_conditions(self) -> dict: """Get current weather conditions at HKO headquarters.""" self._current_vars = [ "temperature_2m", "relative_humidity_2m", "apparent_temperature", "precipitation", "rain", "cloud_cover", "wind_speed_10m", "wind_direction_10m", "wind_gusts_10m", "surface_pressure", "weather_code", ] params = { **self.params, "current": self._current_vars, "forecast_days": 1, } try: responses = self.client.weather_api(self.BASE_URL + "forecast", params=params) current = responses[0].Current() result = {"timestamp": datetime.now().isoformat()} for i in range(current.VariablesLength()): if i < len(self._current_vars): result[self._current_vars[i]] = current.Variables(i).Value() return { "temperature": result.get("temperature_2m"), "humidity": result.get("relative_humidity_2m"), "apparent_temp": result.get("apparent_temperature"), "precipitation": result.get("precipitation"), "rain": result.get("rain"), "cloud_cover": result.get("cloud_cover"), "wind_speed": result.get("wind_speed_10m"), "wind_direction": result.get("wind_direction_10m"), "wind_gusts": result.get("wind_gusts_10m"), "surface_pressure": result.get("surface_pressure"), "weather_code": result.get("weather_code"), "timestamp": datetime.now().isoformat(), } except Exception as e: print(f"Open-Meteo current conditions error: {e}") return {} def get_precipitation_probability(self, hours_ahead: int = 24) -> float: """Get precipitation probability for next N hours.""" df = self.get_forecast(lead_days=2) if df is not None and hasattr(df, 'attrs') and 'hourly' in df.attrs: hourly = df.attrs['hourly'] now = pd.Timestamp.now(tz="Asia/Hong_Kong") future = hourly[hourly.index <= now + pd.Timedelta(hours=hours_ahead)] if 'precipitation_probability' in future.columns: return float(future['precipitation_probability'].max()) return 0.0 def get_scoring_window_summary(self, target_date: str) -> dict: """ Get a forecast summary for a specific scoring window (target date). Used by the signal generator to create trading signals. Returns a dict with all relevant forecast variables for market comparison. """ df = self.get_forecast(lead_days=7) if df is None: return {} target = pd.Timestamp(target_date).tz_localize("Asia/Hong_Kong") if target not in df.index: return {} row = df.loc[target] hourly = df.attrs.get("hourly", pd.DataFrame()) if not hourly.empty: day_hourly = hourly[ (hourly.index >= target) & (hourly.index < target + pd.Timedelta(days=1)) ] else: day_hourly = pd.DataFrame() summary = { "date": target_date, "model": self.model, "fetch_time": df.attrs.get("fetch_time", datetime.now()).isoformat(), "temperature_2m_max": float(row.get("temperature_2m_max", np.nan)), "temperature_2m_min": float(row.get("temperature_2m_min", np.nan)), "precipitation_sum": float(row.get("precipitation_sum", 0)), "precipitation_probability_max": float(row.get("precipitation_probability_max", 0)), "wind_speed_10m_max": float(row.get("wind_speed_10m_max", np.nan)), "wind_gusts_10m_max": float(row.get("wind_gusts_10m_max", np.nan)), } if not day_hourly.empty: summary["temperature_2m_max_hourly"] = float(day_hourly["temperature_2m"].max()) if "temperature_2m" in day_hourly else np.nan summary["precipitation_probability_max_hourly"] = float(day_hourly["precipitation_probability"].max()) if "precipitation_probability" in day_hourly else 0 summary["precipitation_sum_hourly"] = float(day_hourly["precipitation"].sum()) if "precipitation" in day_hourly else 0 return summary