Files
hk-weather-mkt/weather/openmeteo_client.py
ramseshk e59076ad6a Add web dev dashboard with real-time HK weather + model comparison
- Flask web server with interactive charts (Chart.js)
- Temperature, rain probability, wind speed charts with HKO vs Open-Meteo comparison
- Current conditions card with typhoon signal indicator
- Kelly criterion sizing simulator panel
- Trading signals panel with model-implied probabilities
- Raw forecast data JSON viewer
- Github-dark theme with responsive card layout
- Fixed Open-Meteo SDK compatibility and numpy bool serialization

Run: python web_dashboard.py  # http://localhost:5000
2026-08-10 17:11:49 +08:00

254 lines
10 KiB
Python

"""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["fetch_time"] = datetime.now()
# Store hourly separately to avoid pandas attrs bug with DataFrames
self._last_hourly = hourly_df
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:
hourly = getattr(self, '_last_hourly', pd.DataFrame())
if not hourly.empty:
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 = getattr(self, '_last_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