HK Weather Prediction Market Pipeline: WeatherNext + HKO + Polymarket
- Open-Meteo WeatherNext API client for HK forecasts - HKO public data client (current conditions, 9-day forecast, typhoon warnings) - HK-specific weather extraction and calibration - Polymarket market scanning, price discovery, and market creation proposals - Trading strategy engine: edge detection, Kelly criterion sizing, probability calibration - End-to-end pipeline with dry-run mode and scheduled runner - Interactive dashboard with live HK weather + forecasts + trading signals Dependencies: Python 3.10+, openmeteo-requests, pandas No API keys needed for dry-run mode. Polymarket trading requires private key in .env.
This commit is contained in:
@@ -0,0 +1,250 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user