e59076ad6a
- 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
254 lines
10 KiB
Python
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
|